diff --git a/.editorconfig b/.editorconfig index bcfc7d9d..d275d308 100644 --- a/.editorconfig +++ b/.editorconfig @@ -209,7 +209,7 @@ resharper_co_variant_array_conversion_highlighting = suggestion resharper_double_negation_operator_highlighting = warning resharper_event_never_subscribed_to_global_highlighting = hint resharper_field_can_be_made_read_only_local_highlighting = warning -resharper_for_can_be_converted_to_foreach_highlighting = warning +resharper_for_can_be_converted_to_foreach_highlighting = suggestion resharper_function_complexity_overflow_highlighting = warning resharper_incorrect_blank_lines_near_braces_highlighting = warning resharper_inline_out_variable_declaration_highlighting = warning diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 393e1629..08d56673 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,7 +16,10 @@ - Keep AdderProgram in the Tests project and use TestPackage (or Strict.Base) for basic types. ## Strict Semantics -- When asked about Strict semantics, derive behavior directly from README.md and TestPackage.cs examples; re-check cited examples before answering and avoid contradicting them. +- When asked about Strict semantics, derive behavior directly from README.md and Strict/TestPackage examples; re-check cited examples before answering and avoid contradicting them. + +## Strict.Runtime Guidelines +- Prefer ValueInstance-backed representations and avoid object-based value/rawValue conversions where possible. ## Test-Driven Development (TDD) diff --git a/.gitignore b/.gitignore index 304a7229..fe03be5f 100644 --- a/.gitignore +++ b/.gitignore @@ -127,5 +127,4 @@ ipch/ *.sap /Strict_NCrunchResults /.NCrunch_Strict -Base.zip /coverage-results diff --git a/Any.strict b/Any.strict new file mode 100644 index 00000000..2a438aa1 --- /dev/null +++ b/Any.strict @@ -0,0 +1,5 @@ +from +to Type +to Text +to HashCode +is(other) Boolean \ No newline at end of file diff --git a/App.strict b/App.strict new file mode 100644 index 00000000..cb987a18 --- /dev/null +++ b/App.strict @@ -0,0 +1 @@ +Run \ No newline at end of file diff --git a/Strict.Base/Boolean.strict b/Boolean.strict similarity index 94% rename from Strict.Base/Boolean.strict rename to Boolean.strict index e15ae2ee..b7fa0498 100644 --- a/Strict.Base/Boolean.strict +++ b/Boolean.strict @@ -21,4 +21,4 @@ is(other) Boolean to Text true to Text is "true" not true to Text is "false" - value then "true" else "false" + value then "true" else "false" \ No newline at end of file diff --git a/Character.strict b/Character.strict new file mode 100644 index 00000000..eed712c2 --- /dev/null +++ b/Character.strict @@ -0,0 +1,22 @@ +has number +constant zeroCharacter = 48 +constant NewLine = Character(13) +constant Tab = Character(7) +from(text) + Character("b") is "b" + Character(7) is Tab + 7 to Character is "7" + Character("ab") is Error + text.Characters(0) +to Text + Character("a") to Text is "a" + "" + value ++(other) Text + Character("1") + Character("2") is "12" + value to Text + other +to Number + Character("3") to Number is 3 + constant notANumber = Error + Character("A") to Number is notANumber + let result = number - zeroCharacter + result is in Range(0, 10) then result else notANumber(value) \ No newline at end of file diff --git a/Dictionary.strict b/Dictionary.strict new file mode 100644 index 00000000..108cfca5 --- /dev/null +++ b/Dictionary.strict @@ -0,0 +1,32 @@ +has keysAndValues List(key Generic, mappedValue Generic) +from + Dictionary(Number, Number).Length is 0 + Dictionary(Number, Number) is Dictionary(Number, Number) + Dictionary(Number, Number) is not Dictionary(Number, Text) + Dictionary((1, 1)).Length is 1 + Dictionary((1, 1), (2, 2)).Length is 2 +in(key Generic) Boolean + 2 is in Dictionary((1, 1), (2, 2)) + 3 is not in Dictionary((1, 1), (2, 2)) + for keysAndValues + value.Key is key +get(key Generic) Generic + constant abbreviations = Dictionary(("A", "Apple"), ("B", "Ball")) + abbreviations("A") is "Apple" + abbreviations("B") is "Ball" + constant KeyNotFound = Error + abbreviations("C") is KeyNotFound + for keysAndValues + if value.Key is key + return value.Value + KeyNotFound +Add(key Generic, mappedValue Generic) Mutable(Dictionary) + constant DuplicateKey = Error + Dictionary((1, 1)).Add(1, 1) is DuplicateKey + Dictionary((1, 1)).Add(1, 2) is DuplicateKey + Dictionary((2, 4)).Add(4, 8) is Dictionary((2, 4), (4, 8)) + Dictionary((1, 1), (2, 2)).Add(3, 3).Length is 3 + in(key) then DuplicateKey else keysAndValues.Add((key, mappedValue)) +Remove(key Generic) Mutable(Dictionary) + Dictionary((1, 1), (2, 2)).Remove(1).Length is 1 + keysAndValues.Remove((key, get(key))) \ No newline at end of file diff --git a/Directory.strict b/Directory.strict new file mode 100644 index 00000000..fdba0a9e --- /dev/null +++ b/Directory.strict @@ -0,0 +1,3 @@ +from(text) +GetFiles Texts +GetDirectories Texts \ No newline at end of file diff --git a/Enum.strict b/Enum.strict new file mode 100644 index 00000000..3ea50c91 --- /dev/null +++ b/Enum.strict @@ -0,0 +1,9 @@ +has iterator +GetName(type) Text + type.Name +GetName(member) Text + member.Name +CompareTo(member) Number + value > member.Value then 1 else value < member.Value then -1 else 0 +Equals(member) Boolean + value is member.Value \ No newline at end of file diff --git a/Error.strict b/Error.strict new file mode 100644 index 00000000..30bfe471 --- /dev/null +++ b/Error.strict @@ -0,0 +1,2 @@ +has Name +has Stacktraces \ No newline at end of file diff --git a/ErrorWithValue.strict b/ErrorWithValue.strict new file mode 100644 index 00000000..724d4581 --- /dev/null +++ b/ErrorWithValue.strict @@ -0,0 +1,2 @@ +has Error +has Value Generic \ No newline at end of file diff --git a/Examples/ArithmeticFunction.strict b/Examples/ArithmeticFunction.strict new file mode 100644 index 00000000..fc5b3ad7 --- /dev/null +++ b/Examples/ArithmeticFunction.strict @@ -0,0 +1,11 @@ +has numbers +Calculate(operation Text) Number + ArithmeticFunction(10, 5).Calculate("add") is 15 + ArithmeticFunction(10, 5).Calculate("subtract") is 5 + ArithmeticFunction(10, 5).Calculate("multiply") is 50 + for numbers + if operation is + "add" then value + "subtract" then - value + "multiply" then * value + "divide" then / value \ No newline at end of file diff --git a/Examples/Beeramid.strict b/Examples/Beeramid.strict new file mode 100644 index 00000000..b7454a9d --- /dev/null +++ b/Examples/Beeramid.strict @@ -0,0 +1,14 @@ +has bonus Number +has price Number +GetCompleteLevelCount Number + Beeramid(10, 3).GetCompleteLevelCount is 1 + Beeramid(20, 4).GetCompleteLevelCount is 2 + Beeramid(100, 5).GetCompleteLevelCount is 3 + CalculateCompleteLevelCount(bonus / price, 0) +CalculateCompleteLevelCount(numberOfCans Number, levelCount Number) Number + CalculateCompleteLevelCount(3, 0) is 1 + CalculateCompleteLevelCount(5, 0) is 2 + CalculateCompleteLevelCount(20, 1) is 3 + let remainingCans = numberOfCans - levelCount * levelCount + let nextLevelSquare = (levelCount + 1) * (levelCount + 1) + remainingCans < nextLevelSquare then levelCount else CalculateCompleteLevelCount(remainingCans, levelCount + 1) \ No newline at end of file diff --git a/Examples/ConvertingNumbers.strict b/Examples/ConvertingNumbers.strict new file mode 100644 index 00000000..e86df40f --- /dev/null +++ b/Examples/ConvertingNumbers.strict @@ -0,0 +1,9 @@ +has numbers +GetComplicatedSequenceTexts Texts + ConvertingNumbers(1, 21).GetComplicatedSequenceTexts is ("16") + for numbers + to Text + Length * Length + if value > 3 + 4 + value * 3 + to Text \ No newline at end of file diff --git a/Examples/EvenFibonacci.strict b/Examples/EvenFibonacci.strict new file mode 100644 index 00000000..3784a63f --- /dev/null +++ b/Examples/EvenFibonacci.strict @@ -0,0 +1,14 @@ +has number +Sum Number + mutable first = 1 + mutable second = 1 + mutable sum = 0 + for Range(1, number) + let next = first + second + if next >= number + return sum + if next % 2 is 0 + sum.Increment(next) + first = second + second = next + sum \ No newline at end of file diff --git a/Examples/ExecuteOperation.strict b/Examples/ExecuteOperation.strict new file mode 100644 index 00000000..a3dcb1f7 --- /dev/null +++ b/Examples/ExecuteOperation.strict @@ -0,0 +1,12 @@ +has registers +TryOperationExecution(statement) Register + if registers.Length < 2 + Error "OperandsRequired" + GetOperationResult(statement, registers(1), registers(0)) + registers(1) +GetOperationResult(statement, left Register, right Register) Number + if statement.Instruction is + Instruction.Add then left + right + Instruction.Subtract then left - right + Instruction.Multiply then left * right + Instruction.Divide then left / right \ No newline at end of file diff --git a/Examples/Fibonacci.strict b/Examples/Fibonacci.strict new file mode 100644 index 00000000..c4f403bf --- /dev/null +++ b/Examples/Fibonacci.strict @@ -0,0 +1,11 @@ +has number +GetNthFibonacci Number + Fibonacci(5).GetNthFibonacci is 5 + Fibonacci(10).GetNthFibonacci is 55 + mutable first = 1 + mutable second = 1 + for Range(2, number) + let next = first + second + first = second + second = next + second \ No newline at end of file diff --git a/Examples/FindFirstNonConsecutiveNumber.strict b/Examples/FindFirstNonConsecutiveNumber.strict new file mode 100644 index 00000000..dd89aa78 --- /dev/null +++ b/Examples/FindFirstNonConsecutiveNumber.strict @@ -0,0 +1,8 @@ +has numbers +FirstNonConsecutive Number + FindFirstNonConsecutiveNumber(1, 2, 3, 5, 6).FirstNonConsecutive is 5 + FindFirstNonConsecutiveNumber(1, 2, 3).FirstNonConsecutive is 0 + for numbers + if index > 0 and value is not numbers(index - 1) + 1 + return value + 0 \ No newline at end of file diff --git a/Examples/FindNumberCount.strict b/Examples/FindNumberCount.strict new file mode 100644 index 00000000..4d871416 --- /dev/null +++ b/Examples/FindNumberCount.strict @@ -0,0 +1,5 @@ +has numbers +FindNumberCount Number + NumberCount(5, 10).FindNumberCount is 5 + for Range(numbers(0), numbers(1)) + 1 \ No newline at end of file diff --git a/Examples/Instruction.strict b/Examples/Instruction.strict new file mode 100644 index 00000000..eafef6a4 --- /dev/null +++ b/Examples/Instruction.strict @@ -0,0 +1,17 @@ +constant Set +constant Add +constant Subtract +constant Multiply +constant Divide +constant BinaryOperatorsSeparator = 100 +constant GreaterThan +constant LessThan +constant Equal +constant NotEqual +constant ConditionalSeparator = 200 +constant JumpIfTrue +constant JumpIfFalse +constant JumpIfNotZero +constant JumpsSeparator = 300 +constant LoopIfNonZero +constant JumpBackIfNonZero \ No newline at end of file diff --git a/Examples/LinkedListAnalyzer.strict b/Examples/LinkedListAnalyzer.strict new file mode 100644 index 00000000..679af457 --- /dev/null +++ b/Examples/LinkedListAnalyzer.strict @@ -0,0 +1,20 @@ +mutable visited Nodes +GetChainedNode(number) Node + constant head = Node + mutable current = head + for Range(1, number) + current.Next = Node + current = current.Next + current.Next = head +GetLoopLength(node) Number + mutable first = Node + mutable second = Node + first.Next = second + second.Next = first + GetLoopLength(first) is 2 + mutable third = Node + second.Next = third + third.Next = first + GetLoopLength(first) is 3 + visited.Add(node) + visited.Contains(node.Next) then visited.Length else GetLoopLength(node.Next) \ No newline at end of file diff --git a/Examples/MatchingLoopFinder.strict b/Examples/MatchingLoopFinder.strict new file mode 100644 index 00000000..b93de682 --- /dev/null +++ b/Examples/MatchingLoopFinder.strict @@ -0,0 +1,10 @@ +has instructions +GoToMatchingLoopBracket(direction Number, mutable index Number) + mutable loopDepth = direction + for Range(index + direction, instructions.Length) + if instructions(index) is + Instruction.LoopIfNonZero then loopDepth.Increase + Instruction.JumpBackIfNonZero then loopDepth.Decrease + if loopDepth is 0 + return 0 + loopDepth \ No newline at end of file diff --git a/Examples/Node.strict b/Examples/Node.strict new file mode 100644 index 00000000..4f0d924c --- /dev/null +++ b/Examples/Node.strict @@ -0,0 +1,2 @@ +mutable Next Node +has unused Number \ No newline at end of file diff --git a/Examples/Processor.strict b/Examples/Processor.strict new file mode 100644 index 00000000..08cdb43b --- /dev/null +++ b/Examples/Processor.strict @@ -0,0 +1,10 @@ +has progress Number +IsJobDone Boolean or Text + Processor(100).IsJobDone is true + Processor(78).IsJobDone is false + Processor(0).IsJobDone is "Work not started yet" + if progress is 100 + return true + if progress > 0 + return false + "Work not started yet" \ No newline at end of file diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 00000000..721fb5c1 --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,2 @@ +# Strict.Examples +This repository contains example strict programs which demonstrates the currently available features in Strict language. Little bit of a playground for katas and for testing new syntax and language features all the time. diff --git a/Examples/ReduceButGrow.strict b/Examples/ReduceButGrow.strict new file mode 100644 index 00000000..6ed10257 --- /dev/null +++ b/Examples/ReduceButGrow.strict @@ -0,0 +1,7 @@ +has numbers +GetMultiplicationOfNumbers Number + ReduceButGrow(2, 3, 4, 5).GetMultiplicationOfNumbers is 120 + ReduceButGrow(120, 5, 40, 0).GetMultiplicationOfNumbers is 0 + ReduceButGrow(2, 2, 2, 2).GetMultiplicationOfNumbers is 16 + for numbers + * value \ No newline at end of file diff --git a/Examples/Register.strict b/Examples/Register.strict new file mode 100644 index 00000000..f10960ee --- /dev/null +++ b/Examples/Register.strict @@ -0,0 +1,15 @@ +has RZero Number +has ROne Number +has RTwo Number ++(other) Number + Register(0, 1, 2) + Register(1, 2, 3) is Register(1, 3, 5) + value + other +-(other) Number + Register(5, 5, 5) - Register(5, 5, 5) is Register(0, 0, 0) + value - other +*(other) Number + Register(0, 1, 2) * Register(1, 2, 3) is Register(0, 2, 6) + value * other +/(other) Number + Register(5, 5, 5) / Register(5, 5, 5) is Register(1, 1, 1) + value / other \ No newline at end of file diff --git a/Examples/RemoveDuplicateWords.strict b/Examples/RemoveDuplicateWords.strict new file mode 100644 index 00000000..8355f4b5 --- /dev/null +++ b/Examples/RemoveDuplicateWords.strict @@ -0,0 +1,8 @@ +has texts +Remove Texts + RemoveDuplicateWords("a", "b", "b").Remove is ("a", "b") + RemoveDuplicateWords("a", "b", "c").Remove is ("a", "b", "c") + RemoveDuplicateWords("hello", "hi", "hiiii", "hello").Remove is ("hello", "hi", "hiiii") + for texts + if texts.Count(value) > 1 + texts.Remove(value) \ No newline at end of file diff --git a/Examples/RemoveExclamation.strict b/Examples/RemoveExclamation.strict new file mode 100644 index 00000000..3749b714 --- /dev/null +++ b/Examples/RemoveExclamation.strict @@ -0,0 +1,8 @@ +has text +Remove Text + RemoveExclamation("Hello There!").Remove is "Hello There" + RemoveExclamation("Hi!!!").Remove is "Hi" + RemoveExclamation("Wow! Awesome! There!").Remove is "Wow Awesome There" + for text + if value is not "!" + value \ No newline at end of file diff --git a/Examples/RemoveParentheses.strict b/Examples/RemoveParentheses.strict new file mode 100644 index 00000000..180199bb --- /dev/null +++ b/Examples/RemoveParentheses.strict @@ -0,0 +1,11 @@ +has text +Remove Text + RemoveParentheses("example(unwanted thing)example").Remove is "exampleexample" + mutable parentheses = 0 + for text + if value is "(" + parentheses.Increase + else if value is ")" + parentheses.Decrease + else if parentheses is 0 + value \ No newline at end of file diff --git a/Examples/ReverseList.strict b/Examples/ReverseList.strict new file mode 100644 index 00000000..6f1b082c --- /dev/null +++ b/Examples/ReverseList.strict @@ -0,0 +1,5 @@ +has numbers +Reverse Numbers + ReverseList(1, 2, 3, 4).Reverse is (4, 3, 2, 1) + for Range(0, numbers.Length).Reverse + numbers(index) \ No newline at end of file diff --git a/Examples/ShorterPath.strict b/Examples/ShorterPath.strict new file mode 100644 index 00000000..7a68ffdd --- /dev/null +++ b/Examples/ShorterPath.strict @@ -0,0 +1,28 @@ +has target Vector2 +from(path Text) + target = Vector2(path.Count("E") - path.Count("W"), path.Count("N") - path.Count("S")) + for path + if value is + "N" then target.Y.Increment + "S" then target.Y.Decrement + "E" then target.X.Increment + "W" then target.X.Decrement +GetDirections Text + ShorterPath("").GetDirections is "" + ShorterPath("NNE").GetDirections is "NNE" + ShorterPath("NNEN").GetDirections is "NNNE" + ShorterPath("SSNEWSN").GetDirections is "S" + ShorterPath("NWSE").GetDirections is "" + GetVerticalDirections + GetHorizontalDirections +GetVerticalDirections Text + ShorterPath("").GetVerticalDirections is "" + ShorterPath("NN").GetVerticalDirections is "NN" + ShorterPath("NSN").GetVerticalDirections is "N" + for target.Y + target.Y > 0 then "N" else "S" +GetHorizontalDirections Text + ShorterPath("").GetHorizontalDirections is "" + ShorterPath("EEEW").GetVerticalDirections is "EE" + ShorterPath("WWWE").GetVerticalDirections is "WW" + for target.X + target.X > 0 then "E" else "W" \ No newline at end of file diff --git a/Examples/Statement.strict b/Examples/Statement.strict new file mode 100644 index 00000000..c68a695d --- /dev/null +++ b/Examples/Statement.strict @@ -0,0 +1,2 @@ +has Instruction +has Registers \ No newline at end of file diff --git a/Examples/csTranspilerOutput/ArithmeticFunction.cs b/Examples/csTranspilerOutput/ArithmeticFunction.cs new file mode 100644 index 00000000..e5da72bd --- /dev/null +++ b/Examples/csTranspilerOutput/ArithmeticFunction.cs @@ -0,0 +1,25 @@ +namespace SourceGeneratorTests; + +public class ArithmeticFunction +{ + private List numbers = new List(); + public int Calculate(string operation) + { + if (operation == "add") + return numbers[0] + numbers[1]; + if (operation == "subtract") + return numbers[0] - numbers[1]; + if (operation == "multiply") + return numbers[0] * numbers[1]; + if (operation == "divide") + return numbers[0] / numbers[1]; + } + + [Test] + public void CalculateTest() + { + Assert.That(() => new ArithmeticFunction(10, 5).Calculate("add") == 15)); + Assert.That(() => new ArithmeticFunction(10, 5).Calculate("subtract") == 5)); + Assert.That(() => new ArithmeticFunction(10, 5).Calculate("multiply") == 50)); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/Beeramid.cs b/Examples/csTranspilerOutput/Beeramid.cs new file mode 100644 index 00000000..5722a38d --- /dev/null +++ b/Examples/csTranspilerOutput/Beeramid.cs @@ -0,0 +1,23 @@ +namespace SourceGeneratorTests; + +public class Beeramid +{ + public Beeramid(double bonus, double price) + { + this.bonus = bonus; + this.price = price; + } + + private double bonus; + private double price; + public int GetCompleteLevelCount() => + CalculateCompleteLevelCount(bonus / price, 0); + + private static int CalculateCompleteLevelCount(int numberOfCans, int levelCount) + { + var remainingCans = numberOfCans - (levelCount * levelCount); + return remainingCans < ((levelCount + 1) * (levelCount + 1)) + ? levelCount + : CalculateCompleteLevelCount(remainingCans, levelCount + 1); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/ExecuteOperation.cs b/Examples/csTranspilerOutput/ExecuteOperation.cs new file mode 100644 index 00000000..23e820d0 --- /dev/null +++ b/Examples/csTranspilerOutput/ExecuteOperation.cs @@ -0,0 +1,24 @@ +namespace TestPackage; + +public class ExecuteOperation +{ + private List registers = new List(); + public Register TryOperationExecution(Statement statement) + { + if (registers.Length() < 2) + Strict.Language.Expressions.Error TestPackage.Error; + GetOperationResult(statement, registers[1], registers[0]); + registers[1]; + } + public int GetOperationResult(Statement statement, Register left, Register right) + { + if (statement.Instruction == Instruction.Add) + return left + right; + if (statement.Instruction == Instruction.Subtract) + return left - right; + if (statement.Instruction == Instruction.Multiply) + return left * right; + if (statement.Instruction == Instruction.Divide) + return left / right; + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/Fibonacci.cs b/Examples/csTranspilerOutput/Fibonacci.cs new file mode 100644 index 00000000..69d8a71a --- /dev/null +++ b/Examples/csTranspilerOutput/Fibonacci.cs @@ -0,0 +1,26 @@ +namespace TestPackage; + +public class Fibonacci +{ + private int number; + public int GetNthFibonacci() + { + var first = 1; + var second = 1; + var next = 1; + foreach (var index in new Range(2, number)) + { + next = first + second; + first = second; + second = next; + } + return next; + } + + [Test] + public void GetNthFibonacciTest() + { + Assert.That(() => new Fibonacci(5).GetNthFibonacci() == 5)); + Assert.That(() => new Fibonacci(10).GetNthFibonacci() == 55)); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/LinkedListAnalyzer.cs b/Examples/csTranspilerOutput/LinkedListAnalyzer.cs new file mode 100644 index 00000000..9c1130f2 --- /dev/null +++ b/Examples/csTranspilerOutput/LinkedListAnalyzer.cs @@ -0,0 +1,39 @@ +namespace TestPackage; + +public class LinkedListAnalyzer +{ + private List visited = new List(); + public Node GetChainedNode(int number) + { + var head = new Node(); + var current = head; + foreach (var index in new Range(1, number)) + { + if (index == number) + { + current.Next; + return head; + } + current.Next(); + current = current.Next(); + } + return head; + } + public int GetLoopLength(Node node) + { + var first = new Node(); + var second = new Node(); + first.Next(); + second.Next(); + GetLoopLength(first) == 2; + var third = new Node(); + second.Next(); + third.Next(); + GetLoopLength(first) == 3; + visited.Add(node); + if (visited.Contains(node.Next)) + visited.Length() - visited.Index(node.Next); + else + GetLoopLength(node.Next); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/ReduceButGrow.cs b/Examples/csTranspilerOutput/ReduceButGrow.cs new file mode 100644 index 00000000..fb961f1d --- /dev/null +++ b/Examples/csTranspilerOutput/ReduceButGrow.cs @@ -0,0 +1,20 @@ +namespace TestPackage; + +public class ReduceButGrow +{ + private List numbers = new List(); + public int GetMultiplicationOfNumbers() + { + var sum = 1; + foreach (var index in numbers) + sum *= value; + } + + [Test] + public void GetMultiplicationOfNumbersTest() + { + Assert.That(() => new ReduceButGrow((2, 3, 4, 5)).GetMultiplicationOfNumbers() == 120)); + Assert.That(() => new ReduceButGrow((120, 5, 40, 0)).GetMultiplicationOfNumbers() == 0)); + Assert.That(() => new ReduceButGrow((2, 2, 2, 2)).GetMultiplicationOfNumbers() == 16)); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/RemoveDuplicateWords.cs b/Examples/csTranspilerOutput/RemoveDuplicateWords.cs new file mode 100644 index 00000000..6331c6f4 --- /dev/null +++ b/Examples/csTranspilerOutput/RemoveDuplicateWords.cs @@ -0,0 +1,21 @@ +namespace TestPackage; + +public class RemoveDuplicateWords +{ + private List texts = new List(); + public List Remove() + { + foreach (var index in texts) + if (texts.Count(value) > 1) + texts.Remove(value); + return texts; + } + + [Test] + public void RemoveTest() + { + Assert.That(() => new RemoveDuplicateWords("a", "b", "b").Remove() == ("a", "b"))); + Assert.That(() => new RemoveDuplicateWords("a", "b", "c").Remove() == ("a", "b", "c"))); + Assert.That(() => new RemoveDuplicateWords("hello", "hi", "hiiii", "hello").Remove() == ("hello", "hi", "hiiii"))); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/RemoveExclamation.cs b/Examples/csTranspilerOutput/RemoveExclamation.cs new file mode 100644 index 00000000..398dbfc8 --- /dev/null +++ b/Examples/csTranspilerOutput/RemoveExclamation.cs @@ -0,0 +1,20 @@ +namespace TestPackage; + +public class RemoveExclamation +{ + private string text = new string(); + public string Remove() + { + foreach (var index in text) + if (value is not "!") + value = value + value; + } + + [Test] + public void RemoveTest() + { + Assert.That(() => new RemoveExclamation("Hello There!").Remove() == "Hello There")); + Assert.That(() => new RemoveExclamation("Hi!!!").Remove() == "Hi")); + Assert.That(() => new RemoveExclamation("Wow! Awesome! There!").Remove() == "Wow Awesome")); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/RemoveParentheses.cs b/Examples/csTranspilerOutput/RemoveParentheses.cs new file mode 100644 index 00000000..36b29b8e --- /dev/null +++ b/Examples/csTranspilerOutput/RemoveParentheses.cs @@ -0,0 +1,27 @@ +namespace TestPackage; + +public class RemoveParentheses +{ + private string text = new string(); + public string Remove() + { + var parentheses = 0; + var result = ""; + foreach (var index in text) + if (value == "(") + parentheses = parentheses + 1; + else + if (value == ")") + parentheses = parentheses - 1; + else + if (parentheses == 0) + result = result + value; + result; + } + + [Test] + public void RemoveTest() + { + Assert.That(() => new RemoveParentheses("example(unwanted thing)example").Remove() == "exampleexample")); + } +} \ No newline at end of file diff --git a/Examples/csTranspilerOutput/ReverseList.cs b/Examples/csTranspilerOutput/ReverseList.cs new file mode 100644 index 00000000..2c4ba8fe --- /dev/null +++ b/Examples/csTranspilerOutput/ReverseList.cs @@ -0,0 +1,17 @@ +namespace TestPackage; + +public class ReverseList +{ + private List numbers = new List(); + public List Reverse() + { + foreach (var index in new Range(0, numbers.Length()).Reverse()) + numbers[index]; + } + + [Test] + public void ReverseTest() + { + Assert.That(() => new ReverseList(1, 2, 3, 4).Reverse() == (4, 3, 2, 1))); + } +} \ No newline at end of file diff --git a/File.strict b/File.strict new file mode 100644 index 00000000..5309d4df --- /dev/null +++ b/File.strict @@ -0,0 +1,5 @@ +from(filename Name) +Length Number +Read Text +Write(text) +Delete \ No newline at end of file diff --git a/Generic.strict b/Generic.strict new file mode 100644 index 00000000..30aa8fec --- /dev/null +++ b/Generic.strict @@ -0,0 +1 @@ +from(type) \ No newline at end of file diff --git a/HashCode.strict b/HashCode.strict new file mode 100644 index 00000000..b0cbfd85 --- /dev/null +++ b/HashCode.strict @@ -0,0 +1,8 @@ +has number +from(generic) + HashCode(5).Compute is 5 + HashCode("abc").Compute is 3 + HashCode((1, 2)).Compute is 2 + if value is Number + return value + value is Iterator then value.Length else 0 \ No newline at end of file diff --git a/ImageProcessing/AdjustBrightness.strict b/ImageProcessing/AdjustBrightness.strict new file mode 100644 index 00000000..c09d99dc --- /dev/null +++ b/ImageProcessing/AdjustBrightness.strict @@ -0,0 +1,16 @@ +has brightness Number +Process(mutable image ColorImage) ColorImage + mutable testImage = ColorImage(Size(1, 1), (Color(0, 1, 2))) + AdjustBrightness(0).Process(testImage) is ColorImage(Size(1, 1), (Color(0, 1, 2))) + AdjustBrightness(5).Process(testImage) is ColorImage(Size(1, 1), (Color(5, 6, 7))) + if brightness is 0 + return image + for row, column in image.Size + GetBrightnessAdjustedColor(image.Colors(column * image.Size.Width + row)) +GetBrightnessAdjustedColor(currentColor Color) Color + AdjustBrightness(0).GetBrightnessAdjustedColor(Color(0, 1, 2)) is Color(0, 1, 2) + AdjustBrightness(5).GetBrightnessAdjustedColor(Color(0, 0, 0)) is Color(5, 5, 5) + AdjustBrightness(-5).GetBrightnessAdjustedColor(Color(0, 0, 0)) is Color(0, 0, 0) + Color((currentColor.Red + brightness).Clamp(0, 255), + (currentColor.Green + brightness).Clamp(0, 255), + (currentColor.Blue + brightness).Clamp(0, 255)) \ No newline at end of file diff --git a/ImageProcessing/Color.strict b/ImageProcessing/Color.strict new file mode 100644 index 00000000..557f2469 --- /dev/null +++ b/ImageProcessing/Color.strict @@ -0,0 +1,4 @@ +has Red Number +has Green Number +has Blue Number +has Alpha = 1 \ No newline at end of file diff --git a/ImageProcessing/ColorImage.strict b/ImageProcessing/ColorImage.strict new file mode 100644 index 00000000..d7757924 --- /dev/null +++ b/ImageProcessing/ColorImage.strict @@ -0,0 +1,2 @@ +has Size +mutable Colors \ No newline at end of file diff --git a/Input.strict b/Input.strict new file mode 100644 index 00000000..3c6c6091 --- /dev/null +++ b/Input.strict @@ -0,0 +1 @@ +Read Text \ No newline at end of file diff --git a/Iterator.strict b/Iterator.strict new file mode 100644 index 00000000..c79c29ad --- /dev/null +++ b/Iterator.strict @@ -0,0 +1,3 @@ +for Iterator(Generic) +in(element Generic) Boolean +Length Number \ No newline at end of file diff --git a/List.strict b/List.strict new file mode 100644 index 00000000..de0c0da4 --- /dev/null +++ b/List.strict @@ -0,0 +1,134 @@ +has iterator +has elements Generics +to Text + Numbers to Text is "" + (1, 2) to Text is "(1, 2)" + GetElementsText.SurroundWithParentheses +GetElementsText Text + (1, 3).GetElementsText is "1, 3" + for elements + (index is 0 then "" else ", ") + value +for Iterator + elements +Length Number + (1, 2).Length is 2 + for elements + 1 +is(other) Boolean + (1, 2) is (1, 2) + (1, 2, 3) is not (1, 2) + elements is other.elements ++(other) List + (1, 2, 3) + (4, 5) is (1, 2, 3, 4, 5) + ("Hello", "World") + (1, 2) is ("Hello", "World", "1", "2") + ("1", "2") + (3, 4) is ("1", "2", "3", "4") + ("1", "2") to Numbers + (3, 4) is ("1", "2", "3", "4") + ("3", "4") + (1, 2) to Text is ("3", "4", "(1, 2)") + ("3", "4") + (1, 2) to Texts is ("3", "4", "1", "2") + (1 + 2) * 3 is not 1 + 2 * 3 + (("1", "2") + (3, 4)) to Numbers is (1, 2, 3, 4) + 3 + (4) is (3, 4) + (1) + ("Hi") is Error("Cannot downcast Texts to fit to Numbers") + elements + other.elements +-(other) List + (1, 2, 3) - (3) is (1, 2) + elements - other.elements +/(other) List + (2, 4, 6) / (2) is (1, 2, 3) + (1, 2) / (2, 4) is (0.5, 0.5) + (1) / (20, 10) is (0.05, 0.1) + elements / other.elements +*(other) List + (1, 2) * (3, 5) is (3, 10) + constant listsHaveDifferentDimensions = Error + (1) * (1, 2) is listsHaveDifferentDimensions + if Length is not other.Length + listsHaveDifferentDimensions + elements * other.elements ++(generic) List + (1, 2, 3) + 4 is (1, 2, 3, 4) + ("Hello", "World") + 5 is ("Hello", "World", "5") + elements + generic +-(generic) List + (1, 2, 3) - 3 is (1, 2) + (1, 2, 3) - 4 is (1, 2, 3) + elements - generic +*(generic) List + (1, 2) * 3 is (3, 6) + elements * generic +/(generic) List + (2, 4, 6) / 2 is (1, 2, 3) + (20, 10) / 1 is (0.05, 0.1) + elements / generic +Sum Generic + (1, 2, 3).Sum is 6 + for elements + value +X Generic + (1, 2, 3).X is 1 + ().X is Error + elements(0) +Y Generic + (1, 2).Y is 2 + (1).Y is Error + elements(1) +Z Generic + (1, 2, 3).Z is 3 + (1, 2).Z is Error + elements(2) +W Generic + (1, 2, 3, 4).W is 4 + (1, 2, 3).W is Error + elements(3) +First Generic + (1, 2, 3).First is 1 + elements(0) +Last Generic + (1, 2, 3).Last is 3 + elements(-1) +Key Generic + ("A", "Apple").Key is "A" + elements(0) +Value Generic + ("A", "Apple").Key is "Apple" + elements(1) +in(other Generic) Boolean + 3 is in (1, 2, 3) + 3 is not in (1, 2) + 5 is not in (1, 8) + 5 is in (11, 7, 5, 3, 2) + "b" is in ("a", "b", "c") + "d" is not in ("a", "b", "c") + for elements + value is other +Index(other Generic) Number + (1, 2, 3).Index(2) is 1 + (1, 2, 3).Index(9) is -1 + for elements + if value is other + return index + -1 +Add(element Generic) Mutable(List) + mutable someList = List(Mutable(Number)) + someList.Add(1) + someList.Add(2) + someList is (1, 2) + someList(0) = 5 + someList is (5, 2) + someList.Add(3) is (5, 2, 3) + value = value + element +Remove(element Generic) Mutable(List) + (1, 2, 3).Remove(2) is (1, 3) + value = value - element +Count(searchFor Generic) Number + (1, 2).Count(1) is 1 + (1, 3).Count(2) is 0 + ("Hi", "Hello", "Hi").Count("Hi") is 2 + for elements + if value is searchFor + 1 +Reverse List + (1, 2, 3).Reverse is (3, 2, 1) + (5, 10, 5).Reverse is (5, 10, 5) + for Range(0, value.Length).Reverse + outer.value(index) \ No newline at end of file diff --git a/Logger.strict b/Logger.strict new file mode 100644 index 00000000..bee0fb6d --- /dev/null +++ b/Logger.strict @@ -0,0 +1,3 @@ +has textWriter +Log(text) + textWriter.Write(text) \ No newline at end of file diff --git a/Math/README.md b/Math/README.md new file mode 100644 index 00000000..2ca5a979 --- /dev/null +++ b/Math/README.md @@ -0,0 +1,2 @@ +# Strict.Math +Datatypes to make it easier to work with mathematics, algebra, equations, 2D and 3D trigonometry. \ No newline at end of file diff --git a/Math/Size.strict b/Math/Size.strict new file mode 100644 index 00000000..26867ded --- /dev/null +++ b/Math/Size.strict @@ -0,0 +1,22 @@ +has iterator Iterator(Vector2) +has Width Number with value > 0 +has Height Number with value > 0 +for Iterator(Vector2) + for x in Range(0, Width) + for y in Range(0, Height) + Vector2(x, y) +in(element Vector2) Boolean + true +Area Number + Size(1, 1).Area is 1 + Size(2, 3).Area is 6 + Width * Height +Length Number + Size(1, 2).Area is Size(1, 2).Length + Area ++(number) Size + Size(1, 1) + 1 is Size(2, 2) + Size(Width + number, Height + number) +*(number) Size + Size(1, 1) * 5 is Size(5, 5) + Size(Width * number, Height * number) \ No newline at end of file diff --git a/Math/Vector2.strict b/Math/Vector2.strict new file mode 100644 index 00000000..b40c748d --- /dev/null +++ b/Math/Vector2.strict @@ -0,0 +1,25 @@ +has numbers with Length is 2 +constant One = Vector2(1, 1) +constant Left = Vector2(-1, 0) +constant Right = Vector2(1, 0) +constant Up = Vector2(0, -1) +constant Down = Vector2(0, 1) +X Number + Vector2(1, 2).X is 1 + numbers(0) +Y Number + Vector2(1, 2).Y is 2 + numbers(1) +Length Number + Vector2.Length is 0 + Vector2(3, 4).Length is 5 + (X * X + Y * Y).SquareRoot ++(other) List + Vector2 + Vector2.One is (1, 1) + Vector2(1, 2) + Vector2(4, 5) is (5, 7) + Vector2(X + other.X, Y + other.Y) +Normalize Vector2 + Vector2.One.Normalize is Vector2.One + Vector2(0, 5).Normalize is Vector2(0, 1) + Vector2(3, 4).Normalize.Length is 1 + value / Length \ No newline at end of file diff --git a/Member.strict b/Member.strict new file mode 100644 index 00000000..ccf4df6c --- /dev/null +++ b/Member.strict @@ -0,0 +1,3 @@ +has Name +has Value Generic +has Type \ No newline at end of file diff --git a/Method.strict b/Method.strict new file mode 100644 index 00000000..64cc5982 --- /dev/null +++ b/Method.strict @@ -0,0 +1,2 @@ +has Name +has Type \ No newline at end of file diff --git a/Mutable.strict b/Mutable.strict new file mode 100644 index 00000000..a1177e9f --- /dev/null +++ b/Mutable.strict @@ -0,0 +1 @@ +has generic \ No newline at end of file diff --git a/Name.strict b/Name.strict new file mode 100644 index 00000000..21871456 --- /dev/null +++ b/Name.strict @@ -0,0 +1 @@ +has text with Length > 1 and " " is not in value \ No newline at end of file diff --git a/Number.strict b/Number.strict new file mode 100644 index 00000000..2da9150e --- /dev/null +++ b/Number.strict @@ -0,0 +1,83 @@ +has iterator +to Text + 5 to Text is "5" + 10.1 to Text is "10.1" + for digits + value to Character +digits Numbers + 1.digits is (1) + 123.digits is (1, 2, 3) + let nextPart = (value / 10).Floor + let currentDigit = value % 10 + nextPart is 0 then currentDigit else nextPart.digits + currentDigit +to Character + 5 to Character is "5" + constant canOnlyConvertSingleDigit = Error + 10 to Character is canOnlyConvertSingleDigit + value is in Range(0, 10) then Character(Character.zeroCharacter + value) else canOnlyConvertSingleDigit(value) +Floor Number + 1.Floor is 1 + 2.4.Floor is 2 + value - value % 1 +- Number + -1 is not 1 + -value +and(other) Number + 2 and 6 is 6 + 1 and 8 is 9 + 9 and 1 is 9 + value +or(other) Number + 2 or 3 is 3 + 16 or 8 is 16 + value +xor(other) Number + 1 xor 1 is 0 + 3 xor 2 is 1 + value +is(other) Boolean + 1 is 1 + 2 is not 3 + value is other ++(other) Number + 3 + 4 is 7 + value + other +-(other) Number + -1 * 5 is -5 + 3 - 2 is 1 + value - other +/(other) Number + 0 / 50 is 0 + 1 / 20 is 0.05 + value / other +*(other) Number + 3 * 4 is 12 + value * other +%(other) Number + 100 % 10 is 1 + value % other +^(other) Number + 2 ^ 2 is 4 + value ^ other +>(other) Boolean + 0 > 0 is false + 3 > 1 + value > other +>=(other) Boolean + 0 >= 0 + value >= other +<(other) Boolean + 0 < 0 is false + 1 < 3 + value < other +<=(other) Boolean + 0 <= 0 + value <= other +Increment(amount = 1) Mutable(Number) + Mutable(5).Increment.Decrement.Increment is 6 + Mutable(4).Increment(4) is 8 + value = value + amount +Decrement(amount = 1) Mutable(Number) + Mutable(3).Decrement is 2 + Mutable(-2).Decrement(-7) is -9 + value = value - amount \ No newline at end of file diff --git a/README.md b/README.md index 37edb571..94dd6115 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,35 @@ Strict is a simple-to-understand programming language that not only humans can r The long-term goal is NOT to create just another programming language, but use Strict as the foundation for a higher level language for computers to understand and write code. This is the first step towards a future where computers can write their own code, which is the ultimate goal of Strict. Again, this is very different from other programming languages that are designed for humans to write code but not for computers to understand it. Even though LLMs can be trained to repeat and imitate, which is amazing to see, they still don't understand anything really and make the most ridiculous mistakes on any project bigger than a handful of files.) Strict needs to be very fast, both in execution and writing code (much faster than any existing system), so millions of lines of code can be thought of by computers and be evaluated in real time (seconds). -## Language Server Protocol - -The LSP (Language Server Protocol) implementation here adds support for VSCode, VS2019, IntelliJ and many more IDEs as possible Editors for Strict. +More at https://strict-lang.org ## Layers ![Strict Layers](https://strict-lang.org/img/StrictLayers.svg?sanitize=true) +# Strict library + +This repository is not just the runtime, but contains the standard library. It is one and the same. Any folder or github repository folder that contains .strict files is a package. The main folder contains all types that have special language treatment and are required to build strict packages. It is imported by default in every source file, here are the most used types: +- Boolean +- Number +- Text +- List +- Dictionary +- Range +- Error (with Stacktraces, Type and Methods support) +- Log +- common traits like Any (applies to all types), App, Generic (replaced by concrete type in implementation), Mutable, File, Directory, FileReader, FileWriter, etc. + +That is pretty much it, there are not many more things needed to write code. For more specific things see the Math folder, ImageProcessing, Examples, etc. here in this repository. Keep in mind that Strict is NOT a general purpose language. It is supposed to be writeable, readable and understandable by computers. If you want to build a website, a complex app, database code, a game, etc. you should use external code for that (you can keep the logic in Strict, but it is unlikely that Strict will have those things unless some people work on it and need it). It is quite easy to interface with external code, you can use any web api, C++, C#, Java, Python, etc. libraries. Those are threated as black boxes (thus not longer computer understandable, here we would be limited to LLMs) and will not follow the strict rules in Strict. The main issue here would be if an external library changes, then all your calls to it also have to change, which is less of a problem with Strict code calling Strict code as any changes can be fixed in a more automated manner. + +# How to use +The best way to learn how any expression or base type works is to look at the code, it is very compact and every single method starts with tests on how to use it. This base library is not just defining how the language works, but the documentation in source code form. All tests use the is comparsion (similar to == in c style languages, = is used for assignments), "is true" at the end of any expression is never used. All expressions (not just tests) must evaluate to true, otherwise the execution stops with an error. If an expression is constant (like all tests, but also many other expressions), it will be removed and optimized out after the first run. + +See the Readme.md of the Strict project for more details, how to use and example code. +## Language Server Protocol + +The LSP (Language Server Protocol) implementation here adds support for VSCode, VS2019, IntelliJ and many more IDEs as possible Editors for Strict. + ## Architecture at a Glance (from low to high level) | Layer | Responsibility | When It Runs | Rejects / Optimises | @@ -105,7 +126,7 @@ Only needed code that is actually called is ever parsed, validated, tested and r Happy coding with **Strict** — where there’s exactly one way to do it right. -## Examples (all from Strict.Base/Number.strict) +## Examples (all from Strict/Number.strict) ``` 3 + 4 is 7 2 is not 3 @@ -419,7 +440,7 @@ int result = .Select(n => n * 10) .Sum(); ``` -Haskell +Haskell ``` numList :: [Int] numList = [0..9] diff --git a/Range.strict b/Range.strict new file mode 100644 index 00000000..02a9721a --- /dev/null +++ b/Range.strict @@ -0,0 +1,25 @@ +has iterator +has Start Number +has ExclusiveEnd Number +for Iterator(Number) + value +in(number) Boolean + 3 is in Range(1, 4) + 5 is not in Range(1, 4) + 0 is not in Range(1, 4) + number >= Start and number < ExclusiveEnd +Length Number + Range(0, 5).Length is 5 + Range(2, 18).Length is 16 + ExclusiveEnd - Start +Sum Number + Range(2, 5).Sum is 2 + 3 + 4 + Range(42, 45).Sum is 42 + 43 + 44 + for + value +Reverse Range + Range(0, 5).Reverse is Range(4, -1) + Range(1, 4).Reverse is Range(3, 0) + Range(10, 5).Reverse is Range(6, 11) + Range(-5, -10).Reverse is Range(-9, -4) + Length > 0 then Range(ExclusiveEnd - 1, Start - 1) else Range(ExclusiveEnd + 1, Start + 1) \ No newline at end of file diff --git a/Stacktrace.strict b/Stacktrace.strict new file mode 100644 index 00000000..16cec83e --- /dev/null +++ b/Stacktrace.strict @@ -0,0 +1,8 @@ +has Method +has FilePath Text +has Line Number +to Text + to Text is " at Stacktrace.to in Base\Stacktrace.strict:line 7" + Stacktrace(from) to Text is " at Stacktrace.from in Base\Stacktrace.strict:line 7" + Stacktrace(from).First.Method.Type is value + " at " + Method + " in " + FilePath + ":line " + Line \ No newline at end of file diff --git a/Strict.Compiler.Cuda.Tests/CSharpToCudaTranspilerTests.cs b/Strict.Compiler.Cuda.Tests/CSharpToCudaTranspilerTests.cs index 982e9bef..dcec048b 100644 --- a/Strict.Compiler.Cuda.Tests/CSharpToCudaTranspilerTests.cs +++ b/Strict.Compiler.Cuda.Tests/CSharpToCudaTranspilerTests.cs @@ -38,8 +38,8 @@ public void ParseAddNumbers() Assert.That(type.Name, Is.EqualTo(AddNumbers)); Assert.That(type.Methods, Has.Count.EqualTo(1)); Assert.That(type.Methods[0].Name, Is.EqualTo("Add")); - Assert.That(type.Methods[0].Parameters[1].Type, Is.EqualTo(type.FindType(Base.Number))); - Assert.That(type.Methods[0].ReturnType, Is.EqualTo(type.FindType(Base.Number))); + Assert.That(type.Methods[0].Parameters[1].Type, Is.EqualTo(type.FindType(Type.Number))); + Assert.That(type.Methods[0].ReturnType, Is.EqualTo(type.FindType(Type.Number))); Assert.That(type.Methods[0].GetBodyAndParseIfNeeded().ToString(), Is.EqualTo("return first + second")); } diff --git a/Strict.Compiler.Cuda/CSharpToCudaTranspiler.cs b/Strict.Compiler.Cuda/CSharpToCudaTranspiler.cs index 7872562c..0ab470b3 100644 --- a/Strict.Compiler.Cuda/CSharpToCudaTranspiler.cs +++ b/Strict.Compiler.Cuda/CSharpToCudaTranspiler.cs @@ -49,12 +49,14 @@ private static string GetOperator(string expression) => : ""; private static string GetParameterTextWithNameAndType(Type type) => - type.Methods[0]. - Parameters.Aggregate("", (current, parameter) => current + parameter.Type.Name switch + type.Methods[0].Parameters.Aggregate("", (current, parameter) => current + + parameter.Type.Name switch { - Base.Number when parameter.Name is "Width" or "Height" => "const int " + parameter.Name + ", ", - Base.Number when parameter.Name == "initialDepth" => "const float " + parameter.Name + ", ", - Base.Number => "const float *" + parameter.Name + ", ", + Type.Number when parameter.Name is "Width" or "Height" => "const int " + parameter.Name + + ", ", + Type.Number when parameter.Name == "initialDepth" => "const float " + parameter.Name + + ", ", + Type.Number => "const float *" + parameter.Name + ", ", _ => throw new NotSupportedException(parameter.ToString()) }); @@ -87,7 +89,7 @@ public override BlockExpression ParseMethodBody(Method method) : method.bodyLines.Last().Text.Contains('-') ? "-" : "*"; - var numberType = method.FindType(Base.Number)!; + var numberType = method.FindType(Type.Number)!; var arguments = new Expression[] { new Value(numberType, "second") }; var returnExpression = new Return(new Binary(new Value(numberType, "first"), numberType.GetMethod(binaryOperator, arguments), arguments)); @@ -97,14 +99,14 @@ public override BlockExpression ParseMethodBody(Method method) } public CSharpType(Package strictPackage, string filePath) : base( - strictPackage, new TypeLines(Path.GetFileNameWithoutExtension(filePath), File.ReadAllLines(filePath))) + strictPackage, new TypeLines(Path.GetFileNameWithoutExtension(filePath), + global::System.IO.File.ReadAllLines(filePath))) { - var inputCode = File.ReadAllLines(filePath); var methodName = ""; var returnType = ""; var parameters = new List(); var returnStatement = ""; - foreach (var line in inputCode) + foreach (var line in Lines) { if (HasIgnoredOrEmptyText(line)) continue; diff --git a/Strict.Compiler.Roslyn/CSharpExpressionVisitor.cs b/Strict.Compiler.Roslyn/CSharpExpressionVisitor.cs index 580ae167..515f2068 100644 --- a/Strict.Compiler.Roslyn/CSharpExpressionVisitor.cs +++ b/Strict.Compiler.Roslyn/CSharpExpressionVisitor.cs @@ -15,7 +15,7 @@ private static IEnumerable Indent(IEnumerable> exp public string VisitMethodHeader(Method method, bool isInterface) { var isMainEntryPoint = - method.Type.Members.Any(t => t.Type.Name == Base.App) && method.Name == "Run"; + method.Type.Members.Any(t => t.Type.Name == Type.App) && method.Name == "Run"; var methodName = isMainEntryPoint ? "Main" : method.Name; @@ -45,17 +45,19 @@ public string GetAccessModifier(bool isTrait, Method method, bool isMainEntryPoi public string GetCSharpTypeName(Type type) { - if (type is GenericTypeImplementation genericTypeImplementation) - return $"List<{GetCSharpTypeName(genericTypeImplementation.ImplementationTypes[0])}>"; - return type.Name switch - { - Base.None => "void", - Base.Number => "int", - Base.Text => "string", - Base.Boolean => "bool", - "File" => "FileStream", - _ => type.Name - }; + if (type.IsList) + return $"List<{GetCSharpTypeName(type.GetFirstImplementation())}>"; + if (type.IsNone) + return "void"; + if (type.IsBoolean) + return "bool"; + if (type.IsNumber) + return "int"; //could be double as well + if (type.IsText) + return "string"; + return type.Name == "File" + ? "FileStream" + : type.Name; } private string WriteParameters(Method method) => @@ -85,7 +87,7 @@ protected override string Visit(MethodCall methodCall) if (methodCall.Method.Name == "Read" && methodCall.Instance?.ToString() == "file") result += "ReadToEnd"; //ncrunch: no coverage else if (methodCall.Method.Name is "Write" or "Log" && - methodCall.Instance?.ReturnType.Name is Base.Logger or Base.System) + methodCall.Instance?.ReturnType.Name is Type.Logger or Type.System) result += "WriteLine"; else if (methodCall.Method.Name == Method.From) result += methodCall.Method.Type.Name == "File" @@ -93,7 +95,7 @@ protected override string Visit(MethodCall methodCall) : methodCall.Method.Type.Name; else result += methodCall.Method.Name; - result += "(" + methodCall.Arguments.Select(Visit).ToWordList(); + result += "(" + string.Join(", ", methodCall.Arguments.Select(Visit)); if (methodCall.ReturnType.Name == "File") result += ", FileMode.OpenOrCreate"; result += ")"; @@ -114,17 +116,18 @@ private string VisitMethodCallInstance(MethodCall methodCall) => : ""); protected override string Visit(MemberCall memberCall) => - memberCall.Member.Type.Name is Base.Logger or Base.System + memberCall.Member.Type.Name is Type.Logger or Type.System ? "Console" : memberCall.Instance != null ? memberCall.Instance + "." + memberCall.Member.Name : memberCall.Member.Name; protected override string Visit(Value value) => - value.Data is Type + value.Data.IsValueTypeInstanceType ? GetCSharpTypeName(value.ReturnType) : value.ToString(); + //ncrunch: no coverage start protected override IReadOnlyList VisitFor(For forExpression) { var block = new List { "foreach (var index in " + Visit(forExpression.Iterator) + ")" }; diff --git a/Strict.Compiler.Roslyn/CSharpTypeVisitor.cs b/Strict.Compiler.Roslyn/CSharpTypeVisitor.cs index b61d10c5..f9b5051c 100644 --- a/Strict.Compiler.Roslyn/CSharpTypeVisitor.cs +++ b/Strict.Compiler.Roslyn/CSharpTypeVisitor.cs @@ -1,22 +1,23 @@ using Strict.Language; +using Strict.Validators; using Type = Strict.Language.Type; namespace Strict.Compiler.Roslyn; -public sealed class CSharpTypeVisitor : TypeVisitor +public sealed class CSharpTypeVisitor : Visitor { public CSharpTypeVisitor(Type type) { Name = type.Name; expressionVisitor = new CSharpExpressionVisitor(); - isImplementingApp = type.Members.Any(t => t.Type.Name == Base.App); + isImplementingApp = type.Members.Any(t => t.Type.Name == Type.App); isInterface = type.IsTrait; CreateHeader(type); CreateClass(); foreach (var member in type.Members) - VisitMember(member); + Visit(member); foreach (var method in type.Methods) - VisitMethod(method); + Visit(method); AddTests(); ParsingDone(); } @@ -31,7 +32,7 @@ private void CreateHeader(Type type) foreach (var member in type.Members) if (type.IsTraitImplementation(member.Type)) VisitImplement(member.Type); - FileContent += "namespace " + type.Package.FolderPath + SemicolonAndLineBreak + NewLine; + FileContent += "namespace " + type.Package.FullName + SemicolonAndLineBreak + NewLine; } public string FileContent { get; private set; } = ""; @@ -54,7 +55,7 @@ private void CreateClass() => private static readonly string NewLine = Environment.NewLine; - public void VisitMember(Member member) + protected override void Visit(Member member, object? context = null) { if (member.Name is "logger" or "App") return; @@ -83,16 +84,16 @@ private string BuildInitializationExpression(Member member, string csharpTypeNam private static readonly string SemicolonAndLineBreak = ";" + NewLine; - public void VisitMethod(Method method) + public override void Visit(Method method, bool forceParsingBody = false, object? context = null) { VisitMethodHeader(method); if (!isInterface) - VisitMethodBody(method); + BuildMethodBody(method); } private void VisitMethodHeader(Method method) => FileContent += "\t" + expressionVisitor.VisitMethodHeader(method, isInterface); - private void VisitMethodBody(Method method) + private void BuildMethodBody(Method method) { var body = expressionVisitor.VisitBody(method.GetBodyAndParseIfNeeded()); testExpressions.Add(method.Name, @@ -121,6 +122,7 @@ private void AddTests() { if (!testMethod.Value.Any()) break; + //ncrunch: no coverage start hasTestMethods = true; AddTestExpressions(testMethod); } diff --git a/Strict.Compiler.Roslyn/ExpressionVisitor.cs b/Strict.Compiler.Roslyn/ExpressionVisitor.cs index f849263c..84cadcee 100644 --- a/Strict.Compiler.Roslyn/ExpressionVisitor.cs +++ b/Strict.Compiler.Roslyn/ExpressionVisitor.cs @@ -14,7 +14,7 @@ public IReadOnlyList VisitBody(Expression expression) => Body body => VisitBody(body), If ifExpression => VisitIf(ifExpression), SelectorIf selectorIf => VisitSelectorIf(selectorIf), - For forExpression => VisitFor(forExpression), + For forExpression => VisitFor(forExpression), //ncrunch: no coverage _ => [Visit(expression) + ";"] }; diff --git a/Strict.Compiler.Roslyn/Strict.Compiler.Roslyn.csproj b/Strict.Compiler.Roslyn/Strict.Compiler.Roslyn.csproj index c4fab556..01161102 100644 --- a/Strict.Compiler.Roslyn/Strict.Compiler.Roslyn.csproj +++ b/Strict.Compiler.Roslyn/Strict.Compiler.Roslyn.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -13,6 +13,7 @@ + diff --git a/Strict.Compiler.Roslyn/TypeVisitor.cs b/Strict.Compiler.Roslyn/TypeVisitor.cs deleted file mode 100644 index 2c61b6e1..00000000 --- a/Strict.Compiler.Roslyn/TypeVisitor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Strict.Language; -using Type = Strict.Language.Type; - -namespace Strict.Compiler.Roslyn; - -public interface TypeVisitor //TODO: merge with Validators.Visitor -{ - void VisitImplement(Type type); - void VisitMember(Member member); - void VisitMethod(Method method); - void ParsingDone(); -} \ No newline at end of file diff --git a/Strict.Compiler.Tests/SourceGeneratorTests.cs b/Strict.Compiler.Tests/SourceGeneratorTests.cs index e56ecad4..2d8b6fde 100644 --- a/Strict.Compiler.Tests/SourceGeneratorTests.cs +++ b/Strict.Compiler.Tests/SourceGeneratorTests.cs @@ -11,7 +11,7 @@ public sealed class SourceGeneratorTests : TestCSharpGenerator [Test] public void GenerateCSharpInterface() { - var app = new Type(package, new TypeLines("DummyApp", "Run")).ParseMembersAndMethods(parser); + using var app = new Type(package, new TypeLines("DummyApp", "Run")).ParseMembersAndMethods(parser); var file = generator.Generate(app); Assert.That(file.ToString(), Is.EqualTo(@"namespace SourceGeneratorTests; @@ -176,20 +176,18 @@ private async Task new Type(overridePackage ?? TestPackage.Instance, new TypeLines(programName, await File.ReadAllLinesAsync(Path.Combine(await GetExampleFolder(), - $"{programName}.strict")))).ParseMembersAndMethods(parser); + programName + Type.Extension)))).ParseMembersAndMethods(parser); private static async Task GetExampleFolder() { - const string ExamplesSubFolder = "Examples"; - const string DevelopmentExamplesFolder = - Repositories.StrictDevelopmentFolderPrefix + ExamplesSubFolder; - if (Directory.Exists(DevelopmentExamplesFolder)) - return DevelopmentExamplesFolder; - const string ExamplesPackageName = "Strict.Examples"; - return await Repositories.DownloadAndExtractRepository( - new Uri(Repositories.StrictPrefixUri.AbsoluteUri + ExamplesSubFolder), - ExamplesPackageName). - ConfigureAwait(false); + var packageName = nameof(Strict) + Context.ParentSeparator + "Examples"; + var examplesFolder = Repositories.GetLocalDevelopmentPath(Repositories.StrictOrg, packageName); + if (Directory.Exists(examplesFolder)) + return examplesFolder; + var cacheFolder = Path.Combine(Repositories.CacheFolder, Repositories.StrictOrg, packageName); + await Repositories.DownloadRepositoryStrictFiles(cacheFolder, Repositories.StrictOrg, + packageName).ConfigureAwait(false); + return cacheFolder; } [Test] diff --git a/Strict.Compiler.Tests/Strict.Compiler.Tests.csproj b/Strict.Compiler.Tests/Strict.Compiler.Tests.csproj index 4914110f..853bd72f 100644 --- a/Strict.Compiler.Tests/Strict.Compiler.Tests.csproj +++ b/Strict.Compiler.Tests/Strict.Compiler.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/Strict.Compiler.Tests/TestCSharpGenerator.cs b/Strict.Compiler.Tests/TestCSharpGenerator.cs index e406676a..4849d134 100644 --- a/Strict.Compiler.Tests/TestCSharpGenerator.cs +++ b/Strict.Compiler.Tests/TestCSharpGenerator.cs @@ -16,11 +16,13 @@ public void CreateGenerator() parser = new MethodExpressionParser(); package = new Package(nameof(SourceGeneratorTests)); _ = TestPackage.Instance; - new Type(package, new TypeLines(Base.App, "Run")).ParseMembersAndMethods(parser); - new Type(package, new TypeLines(Base.System, "has textWriter", "Write(text)", - "\ttextWriter.Write(text)")).ParseMembersAndMethods(parser); + new Type(package, new TypeLines(Type.App, "Run")).ParseMembersAndMethods(parser); + new Type(package, + new TypeLines(Type.System, "has textWriter", "Write(text)", "\ttextWriter.Write(text)")). + ParseMembersAndMethods(parser); new Type(package, new TypeLines("Input", "Read Text")).ParseMembersAndMethods(parser); - new Type(package, new TypeLines("Output", "Write(generic) Boolean")).ParseMembersAndMethods(parser); + new Type(package, new TypeLines("Output", "Write(generic) Boolean")). + ParseMembersAndMethods(parser); generator = new CSharpGenerator(); } @@ -33,7 +35,6 @@ public void CreateGenerator() protected Type CreateHelloWorldProgramType() => new Type(package, - new TypeLines("Program", "has App", "has logger", "Run", - "\tlogger.Log(\"Hello World\")")). - ParseMembersAndMethods(parser); + new TypeLines("Program", "has App", "has logger", "Run", + "\tlogger.Log(\"Hello World\")")).ParseMembersAndMethods(parser); } \ No newline at end of file diff --git a/Strict.Expressions.Tests/BinaryTests.cs b/Strict.Expressions.Tests/BinaryTests.cs index 06dde9ee..c77080d4 100644 --- a/Strict.Expressions.Tests/BinaryTests.cs +++ b/Strict.Expressions.Tests/BinaryTests.cs @@ -39,7 +39,7 @@ public void ArgumentsDoNotMatchBinaryOperatorParameters() => public void NoMatchingMethodFound() => Assert.That(() => ParseExpression("true - \"text\""), Throws.Exception.InnerException.InstanceOf().With.InnerException. - Message.Contains("not found for TestPackage.Boolean, available methods")); + Message.Contains("not found for TestPackage/Boolean, available methods")); [Test] public void ConversionTypeNotFound() => @@ -72,10 +72,9 @@ public void ParseComparison() => [Test] public void NestedBinary() => - ParseAndCheckOutputMatchesInput("2 * 5 + 3", - CreateBinary( - CreateBinary(new Number(method, 2), BinaryOperator.Multiply, new Number(method, 5)), - BinaryOperator.Plus, new Number(method, 3))); + ParseAndCheckOutputMatchesInput("2 * 5 + 3", CreateBinary( + CreateBinary(new Number(method, 2), BinaryOperator.Multiply, new Number(method, 5)), + BinaryOperator.Plus, new Number(method, 3))); [TestCase("1 + 2")] [TestCase("1 is 1")] @@ -104,7 +103,7 @@ public void ParseGroupExpressionProducesSameCode(string code) => [Test] public void ParseToOperator() => - Assert.That(((To)ParseExpression("5 to Text")).ConversionType.Name, Is.EqualTo(Base.Text)); + Assert.That(((To)ParseExpression("5 to Text")).ConversionType.Name, Is.EqualTo(Type.Text)); [Test] public void ParsePowerWithMultiplyOperator() => @@ -137,17 +136,17 @@ public void NestedBinaryExpressionsSingleGroup() => public void NestedBinaryExpressionsTwoGroups() => ParseAndCheckOutputMatchesInput("(1 + 2) * (3 + 4) * 5", CreateBinary( - CreateBinary(new Number(method, 1), BinaryOperator.Plus, new Number(method, 2)), - BinaryOperator.Multiply, CreateBinary( - CreateBinary(new Number(method, 3), BinaryOperator.Plus, new Number(method, 4)), - BinaryOperator.Multiply, new Number(method, 5)))); + CreateBinary(new Number(method, 1), BinaryOperator.Plus, new Number(method, 2)), + BinaryOperator.Multiply, + CreateBinary(new Number(method, 3), BinaryOperator.Plus, new Number(method, 4))), + BinaryOperator.Multiply, new Number(method, 5))); [Test] public void HasMatchingLeftAndRightExpressionTypes() { var expression = ParseExpression("(\"a\", \"b\") + \"5\""); Assert.That(expression, Is.InstanceOf()); - Assert.That(((Binary)expression).ReturnType, Is.EqualTo(type.GetType(Base.Text.Pluralize()))); + Assert.That(((Binary)expression).ReturnType, Is.EqualTo(type.GetType(Type.Text.Pluralize()))); } } \ No newline at end of file diff --git a/Strict.Expressions.Tests/BodyTests.cs b/Strict.Expressions.Tests/BodyTests.cs index 0664c1b3..a1bd17db 100644 --- a/Strict.Expressions.Tests/BodyTests.cs +++ b/Strict.Expressions.Tests/BodyTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public sealed class BodyTests : TestExpressions @@ -28,7 +30,7 @@ public void UnknownVariable() => [Test] public void CannotAccessAnotherMethodVariable() { - var program = new Type(new Package(nameof(CannotAccessAnotherMethodVariable)), + var program = new Type(TestPackage.Instance, new TypeLines(nameof(CannotAccessAnotherMethodVariable), // @formatter:off "has logger", @@ -45,13 +47,12 @@ public void CannotAccessAnotherMethodVariable() [Test] public void IsConstant() { - var program = new Type(new Package(nameof(IsConstant)), - new TypeLines(nameof(IsConstant), - // @formatter:off - "has logger", - "Run", - "\tconstant number = 5", - "\tlogger.Log(number + number)")).ParseMembersAndMethods(new MethodExpressionParser()); + var program = new Type(TestPackage.Instance, new TypeLines(nameof(IsConstant), + // @formatter:off + "has logger", + "Run", + "\tconstant number = 5", + "\tlogger.Log(number + number)")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(program.Methods[0].GetBodyAndParseIfNeeded().IsConstant, Is.False); } @@ -134,7 +135,7 @@ public void DuplicateVariableInLowerScopeIsNotAllowed() => [Test] public void ChildBodyReturnsFromThreeTabsToOneDirectly() { - var program = new Type(new Package(nameof(ChildBodyReturnsFromThreeTabsToOneDirectly)), + var program = new Type(TestPackage.Instance, new TypeLines(nameof(ChildBodyReturnsFromThreeTabsToOneDirectly), // @formatter:off "has logger", @@ -142,7 +143,7 @@ public void ChildBodyReturnsFromThreeTabsToOneDirectly() "\tconstant number = 5", "\tfor Range(1, number)", "\t\tif index is number", - "\t\t\tconstant current = index", + "\t\t\tlet current = index", "\t\t\treturn current", "\tnumber")).ParseMembersAndMethods(new MethodExpressionParser()); // @formatter:on diff --git a/Strict.Expressions.Tests/DeclarationTests.cs b/Strict.Expressions.Tests/DeclarationTests.cs index 04b49c10..905f2a0a 100644 --- a/Strict.Expressions.Tests/DeclarationTests.cs +++ b/Strict.Expressions.Tests/DeclarationTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public class DeclarationTests : TestExpressions @@ -6,7 +8,6 @@ public class DeclarationTests : TestExpressions public void CreateParserAndPackage() => parser = new MethodExpressionParser(); private ExpressionParser parser = null!; - private static readonly Package Package = new(nameof(DeclarationTests)); [Test] public void MissingConstantValue() => @@ -52,7 +53,7 @@ public void AssignmentWithNestedBinary() Assert.That(expression.Name, Is.EqualTo("result")); Assert.That(expression.Value, Is.InstanceOf()); var rightExpression = (Number)((Binary)expression.Value).Arguments[0]; - Assert.That(rightExpression.Data, Is.EqualTo(6)); + Assert.That(rightExpression.ToString(), Is.EqualTo("6")); } [Test] @@ -62,11 +63,11 @@ public void AssignmentWithListAddition() var expression = (Declaration)body.Expressions[0]; Assert.That(expression.Name, Is.EqualTo("numbers")); Assert.That(expression.ReturnType.Name, - Is.EqualTo(Base.List + "(" + Base.Number + ")")); + Is.EqualTo(Type.List + "(" + Type.Number + ")")); Assert.That(expression.Value, Is.InstanceOf()); var leftExpression = ((Binary)expression.Value).Instance!; Assert.That(leftExpression.ReturnType.Name, - Is.EqualTo(Base.List + "(" + Base.Number + ")")); + Is.EqualTo(Type.List + "(" + Type.Number + ")")); } [Test] @@ -78,7 +79,7 @@ public void NotAssignment() Assert.That(expression.Value, Is.InstanceOf()); Assert.That(expression.Value.ToString(), Is.EqualTo("not true")); var rightExpression = (expression.Value as Not)!.Instance as Boolean; - Assert.That(rightExpression!.Data, Is.EqualTo(true)); + Assert.That(rightExpression!.ToString(), Is.EqualTo("true")); } [Test] @@ -128,9 +129,9 @@ public void LetWithoutExpressionCannotParse() => [Test] public void AssignmentWithMethodCall() { - // @formatter:off - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(AssignmentWithMethodCall), + // @formatter:off "has logger", "MethodToCall Text", "\t\"Hello World\"", @@ -145,25 +146,27 @@ public void AssignmentWithMethodCall() [Test] public void LocalMethodCallShouldHaveCorrectReturnType() { - var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(LocalMethodCallShouldHaveCorrectReturnType), "has logger", "LocalMethod Text", "\t\"Hello World\"", "Run", "\t\"Random Text\"")).ParseMembersAndMethods(parser); - Assert.That(program.Methods[0].ReturnType.Name, Is.EqualTo(Base.Text)); + Assert.That(program.Methods[0].ReturnType.Name, Is.EqualTo(Type.Text)); } [Test] - public void LetAssignmentWithConstructorCall() => - Assert.That( - ((Declaration)((Body)new Type(Package, - new TypeLines(nameof(LetAssignmentWithConstructorCall), "has logger", - "Run", - "\tconstant file = File(\"test.txt\")", - "\tfile is File")).ParseMembersAndMethods(parser).Methods[0]. - GetBodyAndParseIfNeeded()).Expressions[0]).Value.ToString(), Is.EqualTo("File(\"test.txt\")")); + public void LetAssignmentWithConstructorCall() + { + using var runType = new Type(TestPackage.Instance, + new TypeLines(nameof(LetAssignmentWithConstructorCall), "has logger", + "Run", + "\tconstant file = File(\"test.txt\")", + "\tfile is File")).ParseMembersAndMethods(parser); + Assert.That(((Declaration)((Body)runType.Methods[0].GetBodyAndParseIfNeeded()).Expressions[0]). + Value.ToString(), Is.EqualTo("File(\"test.txt\")")); + } [Test] public void LetUsesConstantValue() => @@ -175,7 +178,7 @@ public void LetUsesConstantValue() => public void ConstantUsesNonConstantValue() => Assert.That(() => { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ConstantUsesNonConstantValue), "has number", "Run", diff --git a/Strict.Expressions.Tests/DictionaryTests.cs b/Strict.Expressions.Tests/DictionaryTests.cs index 3f19d345..d2c940ec 100644 --- a/Strict.Expressions.Tests/DictionaryTests.cs +++ b/Strict.Expressions.Tests/DictionaryTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public sealed class DictionaryTests : TestExpressions @@ -5,13 +7,13 @@ public sealed class DictionaryTests : TestExpressions [Test] public void ParseMultipleTypesInsideAListType() { - using var listInListType = new Type(type.Package, + using var listInListType = new Type(TestPackage.Instance, new TypeLines(nameof(ParseMultipleTypesInsideAListType), "has keysAndValues List(key Generic, value Generic)", "UseDictionary", "\tconstant result = 5")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(listInListType.Members[0].Type.IsIterator, Is.True); Assert.That(listInListType.Members[0].Type.Name, - Is.EqualTo("List(key TestPackage.Generic, value TestPackage.Generic)")); + Is.EqualTo("List(key TestPackage/Generic, value TestPackage/Generic)")); var genericType = (GenericType)listInListType.Members[0].Type; Assert.That(genericType.Generic.Name, Is.EqualTo("List")); } @@ -19,7 +21,7 @@ public void ParseMultipleTypesInsideAListType() [Test] public void ParseListWithGenericKeyAndValue() { - using var testType = new Type(type.Package, + using var testType = new Type(TestPackage.Instance, new TypeLines(nameof(ParseListWithGenericKeyAndValue), "has keysAndValues List(key Generic, value Generic)", "Get(key Generic) Generic", "\tfor keysAndValues", "\t\tif value is key", "\t\t\treturn value(1)))")); @@ -29,34 +31,34 @@ public void ParseListWithGenericKeyAndValue() [Test] public void ParseMultipleTypesInsideAListTypeAsParameter() { - using var listInListType = new Type(type.Package, + using var listInListType = new Type(TestPackage.Instance, new TypeLines(nameof(ParseMultipleTypesInsideAListTypeAsParameter), "has keysAndValues Generic", "UseDictionary(keyValues List(firstType Generic, mappedSecondType Generic))", "\tconstant result = 5")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(listInListType.Methods[0].Parameters[0].Type.IsIterator, Is.True); Assert.That(listInListType.Methods[0].Parameters[0].Type.Name, - Is.EqualTo("List(firstType TestPackage.Generic, mappedSecondType TestPackage.Generic)")); + Is.EqualTo("List(firstType TestPackage/Generic, mappedSecondType TestPackage/Generic)")); } [Test] public void ParseDictionaryType() { - using var dictionary = new Type(type.Package, + using var dictionary = new Type(TestPackage.Instance, new TypeLines(nameof(ParseDictionaryType), "has inputMap Dictionary(Number, Number)", "UseDictionary", "\tinputMap.Add(4, 6)", "\tinputMap")). ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(dictionary.Members[0].Type, Is.InstanceOf()); Assert.That(dictionary.Members[0].Type.ToString(), - Is.EqualTo("TestPackage.Dictionary(Number, Number)")); + Is.EqualTo("TestPackage/Dictionary(Number, Number)")); Assert.That(((GenericTypeImplementation)dictionary.Members[0].Type).ImplementationTypes[1], - Is.EqualTo(type.GetType(Base.Number))); + Is.EqualTo(type.GetType(Type.Number))); } [Test] public void DictionaryTupleConstructorParsesAsMethodCall() { - using var dictionary = new Type(type.Package, + using var dictionary = new Type(TestPackage.Instance, new TypeLines(nameof(DictionaryTupleConstructorParsesAsMethodCall), "has number", "Run Dictionary(Number, Number)", "\tDictionary((2, 4))")). ParseMembersAndMethods(new MethodExpressionParser()); @@ -68,7 +70,7 @@ public void DictionaryTupleConstructorParsesAsMethodCall() [Test] public void DictionaryConstructorWithMultiplePairsKeepsText() { - using var dictionary = new Type(type.Package, + using var dictionary = new Type(TestPackage.Instance, new TypeLines(nameof(DictionaryConstructorWithMultiplePairsKeepsText), "has number", "Run Dictionary(Number, Number)", "\tDictionary((2, 4), (4, 8))")). ParseMembersAndMethods(new MethodExpressionParser()); @@ -79,7 +81,7 @@ public void DictionaryConstructorWithMultiplePairsKeepsText() [Test] public void DictionaryTypeExpressionInMemberCallKeepsText() { - using var dictionary = new Type(type.Package, + using var dictionary = new Type(TestPackage.Instance, new TypeLines(nameof(DictionaryTypeExpressionInMemberCallKeepsText), "has number", "Run Boolean", "\tDictionary(Number, Number).Length is 0")). ParseMembersAndMethods(new MethodExpressionParser()); @@ -90,34 +92,35 @@ public void DictionaryTypeExpressionInMemberCallKeepsText() [Test] public void ParseDictionaryWithMixedInputTypes() { - using var dictionary = new Type(type.Package, + using var dictionary = new Type(TestPackage.Instance, new TypeLines(nameof(ParseDictionaryWithMixedInputTypes), "has input Dictionary(Text, Boolean)", "AddToDictionaryAndGetLength Number", "\tinput.Add(\"10\", true)", "\tinput.Length")). ParseMembersAndMethods(new MethodExpressionParser()); dictionary.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(dictionary.Members[0].Type.ToString(), - Is.EqualTo("TestPackage.Dictionary(Text, Boolean)")); + Is.EqualTo("TestPackage/Dictionary(Text, Boolean)")); } [Test] public void AddingIncorrectInputTypesToDictionaryShouldError() { - using var dictionary = new Type(type.Package, + using var dictionary = new Type(TestPackage.Instance, new TypeLines(nameof(AddingIncorrectInputTypesToDictionaryShouldError), "has input Dictionary(Text, Boolean)", "UseDictionary", "\tinput.Add(4, \"10\")", "\tinput")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(() => dictionary.Methods[0].GetBodyAndParseIfNeeded(), Throws.InnerException.InstanceOf().With. InnerException.Message.Contains( - "Arguments: 4 TestPackage.Number, \"10\" TestPackage.Text do not match these TestPackage.Dictionary(Text, Boolean) method(s):" + - "\nAdd(key TestPackage.Text, mappedValue TestPackage.Boolean) Mutable(Dictionary(Text, Boolean))")); + "Arguments: 4 TestPackage/Number, \"10\" TestPackage/Text do not match these " + + "TestPackage/Dictionary(Text, Boolean) method(s):\nAdd(key TestPackage/Text, " + + "mappedValue TestPackage/Boolean) Mutable(Dictionary(Text, Boolean))")); } [Test] public void CreateAndValidateDictionaryTypeInstance() { - using var registerType = new Type(type.Package, + using var registerType = new Type(TestPackage.Instance, new TypeLines("SchoolRegister", "has logger", "LogStudentsDetails", "\tmutable studentsRegister = Dictionary(Number, Text)", "\tstudentsRegister.Add(1, \"AK\")", "\tlogger.Log(studentsRegister)")). @@ -125,15 +128,15 @@ public void CreateAndValidateDictionaryTypeInstance() var body = (Body)registerType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(((Declaration)body.Expressions[0]).Value, Is.InstanceOf()); var dictionaryExpression = (Dictionary)((Declaration)body.Expressions[0]).Value; - Assert.That(dictionaryExpression.KeyType, Is.EqualTo(type.GetType(Base.Number))); - Assert.That(dictionaryExpression.MappedValueType, Is.EqualTo(type.GetType(Base.Text))); + Assert.That(dictionaryExpression.KeyType, Is.EqualTo(type.GetType(Type.Number))); + Assert.That(dictionaryExpression.MappedValueType, Is.EqualTo(type.GetType(Type.Text))); } [Test] public void DictionaryExpressionToStringUsesTypeName() { - var number = type.GetType(Base.Number); - var dictionaryType = type.GetType(Base.Dictionary).GetGenericImplementation(number, number); + var number = type.GetType(Type.Number); + var dictionaryType = type.GetType(Type.Dictionary).GetGenericImplementation(number, number); Assert.That(new Dictionary([number, number], dictionaryType).ToString(), Is.EqualTo("Dictionary(Number, Number)")); } @@ -141,7 +144,7 @@ public void DictionaryExpressionToStringUsesTypeName() [Test] public void DictionaryMustBeInitializedWithTwoTypeParameters() { - using var testType = new Type(type.Package, + using var testType = new Type(TestPackage.Instance, new TypeLines(nameof(DictionaryMustBeInitializedWithTwoTypeParameters), "has logger", "DummyInitialization", "\tmutable studentsRegister = Dictionary(Number, Text, Number)")). @@ -154,7 +157,7 @@ public void DictionaryMustBeInitializedWithTwoTypeParameters() [Test] public void CannotAddMismatchingInputTypesToDictionaryInstance() { - using var testType = new Type(type.Package, + using var testType = new Type(TestPackage.Instance, new TypeLines(nameof(CannotAddMismatchingInputTypesToDictionaryInstance), "has logger", "DummyInitialization", "\tconstant studentsRegister = Dictionary(Number, Boolean)", "\tstudentsRegister.Add(5, \"hi\")", "\tlogger.Log(studentsRegister)")). @@ -170,9 +173,9 @@ public void CannotCreateDictionaryExpressionWithThreeTypeParameters() => () => new Dictionary( new List { - type.GetType(Base.Number), type.GetType(Base.Text), type.GetType(Base.Boolean) + type.GetType(Type.Number), type.GetType(Type.Text), type.GetType(Type.Boolean) }, type), Throws.InstanceOf().With. Message.StartsWith("Expected Type Parameters: 2, Given type parameters: 3 and they are " + - "TestPackage.Number, TestPackage.Text, TestPackage.Boolean")); + "TestPackage/Number, TestPackage/Text, TestPackage/Boolean")); } \ No newline at end of file diff --git a/Strict.Expressions.Tests/ErrorTests.cs b/Strict.Expressions.Tests/ErrorTests.cs index 3cd2cf00..45e53997 100644 --- a/Strict.Expressions.Tests/ErrorTests.cs +++ b/Strict.Expressions.Tests/ErrorTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public sealed class ErrorTests : TestExpressions @@ -5,19 +7,18 @@ public sealed class ErrorTests : TestExpressions [Test] public void ParseErrorExpression() { - var programType = new Type(type.Package, - new TypeLines(nameof(ParseErrorExpression), - "has number", - "CheckNumberInRangeTen Number", - "\tconstant notANumber = Error", - "\tif number is in Range(0, 10)", - "\t\treturn number", - "\telse", - "\t\treturn notANumber")). - ParseMembersAndMethods(new MethodExpressionParser()); + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(ParseErrorExpression), + "has number", + "CheckNumberInRangeTen Number", + "\tconstant notANumber = Error", + "\tif number is in Range(0, 10)", + "\t\treturn number", + "\telse", + "\t\treturn notANumber")).ParseMembersAndMethods(new MethodExpressionParser()); var parsedExpression = (Body)programType.Methods[0].GetBodyAndParseIfNeeded(); var declaration = ((Declaration)parsedExpression.Expressions[0]).Value; - Assert.That(declaration.ReturnType, Is.EqualTo(type.GetType(Base.Error))); + Assert.That(declaration.ReturnType, Is.EqualTo(type.GetType(Type.Error))); Assert.That(((If)parsedExpression.Expressions[1]).OptionalElse?.ToString(), Is.EqualTo("return notANumber")); Assert.That(declaration.ToString(), Is.EqualTo("Error")); @@ -26,42 +27,40 @@ public void ParseErrorExpression() [Test] public void TypeLevelErrorExpression() { - var programType = new Type(type.Package, - new TypeLines(nameof(TypeLevelErrorExpression), - "has number", - "constant NotANumber = Error", - "CheckIfNumberIsInRangeTen Number", - "\tif number is in Range(0, 10)", - "\t\treturn number", - "\telse", - "\t\treturn NotANumber")). - ParseMembersAndMethods(new MethodExpressionParser()); + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(TypeLevelErrorExpression), + "has number", + "constant NotANumber = Error", + "CheckIfNumberIsInRangeTen Number", + "\tif number is in Range(0, 10)", + "\t\treturn number", + "\telse", + "\t\treturn NotANumber")).ParseMembersAndMethods(new MethodExpressionParser()); var ifExpression = (If)programType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(programType.Members[1].Type, - Is.EqualTo(type.GetType(Base.Error))); + Is.EqualTo(type.GetType(Type.Error))); Assert.That(ifExpression.OptionalElse?.ToString(), Is.EqualTo("return NotANumber")); } [Test] public void ErrorTextAndStacktraceIsFilledAutomatically() { - var programType = new Type(type.Package, - new TypeLines(nameof(ErrorTextAndStacktraceIsFilledAutomatically), - "has number", - "Run", - "\tError")). - ParseMembersAndMethods(new MethodExpressionParser()); + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(ErrorTextAndStacktraceIsFilledAutomatically), + "has number", + "Run", + "\tError")).ParseMembersAndMethods(new MethodExpressionParser()); var returnExpression = (MethodCall)programType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(returnExpression.Arguments, Has.Count.EqualTo(2)); Assert.That(returnExpression.Arguments[0].ToString(), Is.EqualTo("\"Run\"")); Assert.That(returnExpression.Arguments[1].ReturnType, - Is.EqualTo(type.GetListImplementationType(type.GetType(Base.Stacktrace)))); + Is.EqualTo(type.GetListImplementationType(type.GetType(Type.Stacktrace)))); } [Test] public void ErrorCanAddDetails() { - var programType = new Type(type.Package, + using var programType = new Type(TestPackage.Instance, new TypeLines(nameof(ErrorCanAddDetails), "has number", "Run", "\tconstant someError = Error", "\tsomeError(number)")). ParseMembersAndMethods(new MethodExpressionParser()); diff --git a/Strict.Expressions.Tests/ForTests.cs b/Strict.Expressions.Tests/ForTests.cs index eb25ffe1..ecbd7a51 100644 --- a/Strict.Expressions.Tests/ForTests.cs +++ b/Strict.Expressions.Tests/ForTests.cs @@ -1,3 +1,4 @@ +using Strict.Language.Tests; using static Strict.Expressions.For; namespace Strict.Expressions.Tests; @@ -6,8 +7,7 @@ public sealed class ForTests : TestExpressions { [Test] public void MissingBody() => - Assert.That(() => ParseExpression("for Range(2, 5)"), - Throws.InstanceOf()); + Assert.That(() => ParseExpression("for Range(2, 5)"), Throws.InstanceOf()); [Test] public void MissingExpression() => @@ -50,8 +50,7 @@ public void ForVariableMatchingMemberIsNotAddedAsVariable() => [TestCase("for gibberish", "\tlogger.Log(\"Hi\")")] [TestCase("for element in gibberish", "\tlogger.Log(element)")] public void UnidentifiedIterable(params string[] lines) => - Assert.That(() => ParseExpression(lines), - Throws.InstanceOf()); + Assert.That(() => ParseExpression(lines), Throws.InstanceOf()); [Test] public void ImmutableVariableNotAllowedToBeAnIterator() => @@ -70,18 +69,14 @@ public void IteratorTypeDoesNotMatchWithIterable() => [Test] public void ForVariableUsesNonListIteratorValue() { - var programType = new Type(type.Package, - new TypeLines(nameof(ForVariableUsesNonListIteratorValue), "has logger", - "LogCount(count Number) Number", - "\tfor element in count", - "\t\tlogger.Log(element)", - "\t\telement", - "\tcount")). - ParseMembersAndMethods(new MethodExpressionParser()); + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(ForVariableUsesNonListIteratorValue), "has logger", + "LogCount(count Number) Number", "\tfor element in count", "\t\tlogger.Log(element)", + "\t\telement", "\tcount")).ParseMembersAndMethods(new MethodExpressionParser()); var body = (Body)programType.Methods[0].GetBodyAndParseIfNeeded(); var forExpression = (For)body.Expressions[0]; Assert.That(((Body)forExpression.Body).FindVariable("element")?.Type.Name, - Is.EqualTo(Base.Number)); + Is.EqualTo(Type.Number)); } [Test] @@ -100,8 +95,8 @@ public void ParseForRangeExpression() => [Test] public void ParseForDictionaryElementsExpression() { - var number = type.GetType(Base.Number); - var dictionary = type.GetType(Base.Dictionary).GetGenericImplementation(number, number); + var number = type.GetType(Type.Number); + var dictionary = type.GetType(Type.Dictionary).GetGenericImplementation(number, number); var runMethod = new Method(dictionary, 0, new MethodExpressionParser(), [ "Run Number", "\tfor elements", @@ -146,18 +141,17 @@ public void NestedIfInForIsIndented() => public void ValidIteratorReturnTypeWithValue() => Assert.That( ((VariableCall)((MethodCall)((For)ParseExpression("for (1, 2, 3)", "\tlogger.Log(value)")). - Body).Arguments[0]).ReturnType.FullName, Is.EqualTo("TestPackage.Number")); + Body).Arguments[0]).ReturnType.FullName, Is.EqualTo("TestPackage/Number")); [TestCase("constant elements = (1, 2, 3)", "for elements", "\tlogger.Log(index)", "for elements\n\tlogger.Log(index)")] - [TestCase("constant elements = (1, 2, 3)", "for Range(0, elements.Length)", "\tlogger.Log(index)", - "for Range(0, elements.Length)\n\tlogger.Log(index)")] + [TestCase("constant elements = (1, 2, 3)", "for Range(0, elements.Length)", + "\tlogger.Log(index)", "for Range(0, elements.Length)\n\tlogger.Log(index)")] [TestCase("mutable element = 0", "for element in (1, 2, 3)", "\tlogger.Log(element)", "for element in (1, 2, 3)\n\tlogger.Log(element)")] [TestCase("constant iterationCount = 10", "for iterationCount", "\tlogger.Log(index)", "for iterationCount\n\tlogger.Log(index)")] - [TestCase("constant dummy = 0", "for 10", "\tlogger.Log(index)", - "for 10\n\tlogger.Log(index)")] + [TestCase("constant dummy = 0", "for 10", "\tlogger.Log(index)", "for 10\n\tlogger.Log(index)")] [TestCase("mutable element = \"1\"", "for element in (\"1\", \"2\", \"3\")", "\tlogger.Log(element)", "for element in (\"1\", \"2\", \"3\")\n\tlogger.Log(element)")] public void ParseForListExpressionWithIterableVariable(params string[] lines) => @@ -168,33 +162,30 @@ public void ParseForListExpressionWithIterableVariable(params string[] lines) => public void ValidIteratorReturnTypeForRange() => Assert.That( ((MethodCall)((For)ParseExpression("for Range(0, 10)", "\tlogger.Log(index)")).Body). - Arguments[0].ReturnType.Name == Base.Number); + Arguments[0].ReturnType.IsNumber); [Test] public void ValidIteratorReturnTypeTextForList() => Assert.That( ((VariableCall)((MethodCall)((For)((Body)ParseExpression("mutable element = \"1\"", "for element in (\"1\", \"2\", \"3\")", "\tlogger.Log(element)")).Expressions[1]).Body). - Arguments[0]).Variable.Type.Name == Base.Text); + Arguments[0]).Variable.Type.IsText); [Test] public void ValidLoopProgram() { - using var programType = new Type(type.Package, - new TypeLines(Base.App, "has number", - "CountNumber Number", - "\tfor Range(0, number)", - "\t\t1")). - ParseMembersAndMethods(new MethodExpressionParser()); + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(ValidLoopProgram), "has number", "CountNumber Number", + "\tfor Range(0, number)", "\t\t1")).ParseMembersAndMethods(new MethodExpressionParser()); var parsedExpression = (For)programType.Methods[0].GetBodyAndParseIfNeeded(); - Assert.That(parsedExpression.ReturnType.Name, Is.EqualTo(Base.Range)); + Assert.That(parsedExpression.ReturnType.Name, Is.EqualTo(Type.Range)); Assert.That(parsedExpression.Iterator.ToString(), Is.EqualTo("Range(0, number)")); } [Test] public void ErrorExpressionIsNotAnIterator() { - var programType = new Type(type.Package, + using var programType = new Type(TestPackage.Instance, new TypeLines(nameof(ErrorExpressionIsNotAnIterator), "has number", "LogError Number", "\tconstant error = Error(\"Process Failed\")", "\tfor error", "\t\tvalue")). ParseMembersAndMethods(new MethodExpressionParser()); @@ -206,7 +197,7 @@ public void ErrorExpressionIsNotAnIterator() [TestCase("error.Text", nameof(IterateErrorTypeMembers) + "Text")] public void IterateErrorTypeMembers(string forExpressionText, string testName) { - var programType = new Type(type.Package, + using var programType = new Type(TestPackage.Instance, new TypeLines(testName, "has number", "LogError Number", "\tconstant error = Error(\"Process Failed\")", $"\tfor {forExpressionText}", "\t\tvalue")).ParseMembersAndMethods(new MethodExpressionParser()); @@ -219,7 +210,7 @@ public void IterateErrorTypeMembers(string forExpressionText, string testName) [Test] public void IterateNameType() { - var programType = new Type(type.Package, + using var programType = new Type(TestPackage.Instance, new TypeLines(nameof(IterateNameType), "has number", "LogError Number", "\tconstant name = Name(\"Strict\")", "\tfor name", "\t\tvalue")). ParseMembersAndMethods(new MethodExpressionParser()); @@ -230,55 +221,35 @@ public void IterateNameType() [Test] public void MissingBodyInNestedFor() => - Assert.That(() => ParseExpression( - "for Range(2, 5)", - "for index in Range(1, 10)"), + Assert.That(() => ParseExpression("for Range(2, 5)", "for index in Range(1, 10)"), Throws.InstanceOf()); - [TestCase( - "WithParameter", "for element in (1, 2, 3, 4)", - "has logger", - "LogError Number", - "\tfor element in (1, 2, 3, 4)", - "\t\tlogger.Log(element)")] - [TestCase( - "WithList", "for element in elements", - "has logger", - "LogError(elements Numbers) Number", - "\tfor element in elements", - "\t\tlogger.Log(element)")] - [TestCase( - "WithListTexts", "for element in texts", - "has logger", - "LogError(texts) Number", - "\tfor element in texts", - "\t\tlogger.Log(element)")] + [TestCase("WithParameter", "for element in (1, 2, 3, 4)", "has logger", "LogError Number", + "\tfor element in (1, 2, 3, 4)", "\t\tlogger.Log(element)")] + [TestCase("WithList", "for element in elements", "has logger", + "LogError(elements Numbers) Number", "\tfor element in elements", "\t\tlogger.Log(element)")] + [TestCase("WithListTexts", "for element in texts", "has logger", "LogError(texts) Number", + "\tfor element in texts", "\t\tlogger.Log(element)")] public void AllowCustomVariablesInFor(string testName, string expected, params string[] code) { - var programType = - new Type(type.Package, new TypeLines(nameof(AllowCustomVariablesInFor) + testName, code)). - ParseMembersAndMethods(new MethodExpressionParser()); + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(AllowCustomVariablesInFor) + testName, code)). + ParseMembersAndMethods(new MethodExpressionParser()); var parsedExpression = (For)programType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(parsedExpression.ToString(), Does.StartWith(expected)); } - [TestCase("WithNumbers", "for row, column in listOfNumbers", - "has logger", - "LogAllNumbers(listOfNumbers List(Numbers))", - "\tfor row, column in listOfNumbers", - "\t\tlogger.Log(column)")] - [TestCase( - "WithTexts", "for row, column in texts", - "has logger", - "LogTexts(texts)", - "\tfor row, column in texts", + [TestCase("WithNumbers", "for row, column in listOfNumbers", "has logger", + "LogAllNumbers(listOfNumbers List(Numbers))", "\tfor row, column in listOfNumbers", "\t\tlogger.Log(column)")] - public void ParseForExpressionWithMultipleVariables(string testName, string expected, params string[] code) + [TestCase("WithTexts", "for row, column in texts", "has logger", "LogTexts(texts)", + "\tfor row, column in texts", "\t\tlogger.Log(column)")] + public void ParseForExpressionWithMultipleVariables(string testName, string expected, + params string[] code) { - var programType = - new Type(type.Package, - new TypeLines(nameof(ParseForExpressionWithMultipleVariables) + testName, code)). - ParseMembersAndMethods(new MethodExpressionParser()); + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(ParseForExpressionWithMultipleVariables) + testName, code)). + ParseMembersAndMethods(new MethodExpressionParser()); var parsedExpression = (For)programType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(parsedExpression.ToString(), Does.StartWith(expected)); } @@ -286,10 +257,9 @@ public void ParseForExpressionWithMultipleVariables(string testName, string expe [Test] public void ForBodyWithInlineConditionalThenConcatenation() { - var programType = new Type(type.Package, + using var programType = new Type(TestPackage.Instance, new TypeLines(nameof(ForBodyWithInlineConditionalThenConcatenation), "has number", - "GetElementsText(elements Numbers) Text", - "\tfor elements", + "GetElementsText(elements Numbers) Text", "\tfor elements", "\t\t(index is 0 then \"\" else \", \") + value")). ParseMembersAndMethods(new MethodExpressionParser()); var forExpression = (For)programType.Methods[0].GetBodyAndParseIfNeeded(); @@ -299,17 +269,11 @@ public void ForBodyWithInlineConditionalThenConcatenation() [Test] public void RemoveParenthesesWithElseIfChain() { - var programType = new Type(type.Package, - new TypeLines(nameof(RemoveParenthesesWithElseIfChain), "has text", - "Remove Text", - "\tmutable parentheses = 0", - "\tfor text", - "\t\tif value is \"(\"", - "\t\t\tparentheses = parentheses + 1", - "\t\telse if value is \")\"", - "\t\t\tparentheses = parentheses - 1", - "\t\telse if parentheses is 0", - "\t\t\tvalue")). + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(RemoveParenthesesWithElseIfChain), "has text", "Remove Text", + "\tmutable parentheses = 0", "\tfor text", "\t\tif value is \"(\"", + "\t\t\tparentheses = parentheses + 1", "\t\telse if value is \")\"", + "\t\t\tparentheses = parentheses - 1", "\t\telse if parentheses is 0", "\t\t\tvalue")). ParseMembersAndMethods(new MethodExpressionParser()); var body = (Body)programType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(body.Expressions[1], Is.TypeOf()); @@ -318,12 +282,9 @@ public void RemoveParenthesesWithElseIfChain() [Test] public void ForBodyMultiLinePiping() { - var programType = new Type(type.Package, - new TypeLines(nameof(ForBodyMultiLinePiping), "has numbers", - "GetNumbersText Numbers", - "\tfor numbers", - "\t\tto Text", - "\t\tLength")). + using var programType = new Type(TestPackage.Instance, + new TypeLines(nameof(ForBodyMultiLinePiping), "has numbers", "GetNumbersText Numbers", + "\tfor numbers", "\t\tto Text", "\t\tLength")). ParseMembersAndMethods(new MethodExpressionParser()); var forExpression = (For)programType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(forExpression.Body.ToString(), Does.Contain("to Text")); diff --git a/Strict.Expressions.Tests/IfAdvancedTests.cs b/Strict.Expressions.Tests/IfAdvancedTests.cs index bda93ede..e6a7f8e3 100644 --- a/Strict.Expressions.Tests/IfAdvancedTests.cs +++ b/Strict.Expressions.Tests/IfAdvancedTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public sealed class IfAdvancedTests : TestExpressions @@ -24,7 +26,7 @@ public void ParseIfElse() => [Test] public void ParseSelectorIf() { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ParseSelectorIf), // @formatter:off "has operation Text", @@ -47,7 +49,7 @@ public void ParseSelectorIf() [Test] public void ParseSelectorIfWithElse() { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ParseSelectorIfWithElse), // @formatter:off "has operation Text", @@ -94,7 +96,7 @@ public void ConditionalExpressionsAsPartOfOtherExpression(string code) => [Test] public void ReturnTypeOfThenMustMatchMethodReturnType() { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ReturnTypeOfThenMustMatchMethodReturnType), "has logger", "InvalidRun Number", @@ -103,36 +105,34 @@ public void ReturnTypeOfThenMustMatchMethodReturnType() " return \"5\"")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(() => program.Methods[0].GetBodyAndParseIfNeeded(), Throws.InstanceOf().With.Message.Contains( - "Last expression return \"5\" return type: TestPackage.Text is not matching with expected " + - "method return type: TestPackage.Number in method line: 3")); + "Last expression return \"5\" return type: TestPackage/Text is not matching with " + + "expected method return type: TestPackage/Number in method line: 3")); } - private static readonly Package Package = new(nameof(IfTests)); - [Test] public void ReturnTypeOfElseMustMatchMethodReturnType() { - var program = new Type(Package, new TypeLines( - nameof(ReturnTypeOfElseMustMatchMethodReturnType), - // @formatter:off - "has logger", - "InvalidRun Number", - " InvalidRun is Number", - " if 5 is 5", - " constant file = File(\"test.txt\")", - " return \"Hello\"", - " 6")).ParseMembersAndMethods(new MethodExpressionParser()); + using var program = new Type(TestPackage.Instance, + new TypeLines(nameof(ReturnTypeOfElseMustMatchMethodReturnType), + // @formatter:off + "has logger", + "InvalidRun Number", + " InvalidRun is Number", + " if 5 is 5", + " constant file = File(\"test.txt\")", + " return \"Hello\"", + " 6")).ParseMembersAndMethods(new MethodExpressionParser()); // @formatter:on Assert.That(() => program.Methods[0].GetBodyAndParseIfNeeded(), Throws.InstanceOf().With.Message.Contains( - "Last expression return \"Hello\" return type: TestPackage.Text is not matching with " + - "expected method return type: TestPackage.Number in method line: 4")); + "Last expression return \"Hello\" return type: TestPackage/Text is not matching with " + + "expected method return type: TestPackage/Number in method line: 4")); } [Test] public void ThenReturnsImplementedTypeOfMethodReturnType() { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ThenReturnsImplementedTypeOfMethodReturnType), // @formatter:off "has logger", @@ -145,13 +145,13 @@ public void ThenReturnsImplementedTypeOfMethodReturnType() // @formatter:on Assert.That( ((Body)program.Methods[0].GetBodyAndParseIfNeeded()).children[0].ReturnType.ToString(), - Is.EqualTo("TestPackage.Number")); + Is.EqualTo("TestPackage/Number")); } [Test] public void MultiLineThenAndElseWithMatchingMethodReturnType() { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MultiLineThenAndElseWithMatchingMethodReturnType), // @formatter:off "has logger", @@ -164,22 +164,23 @@ public void MultiLineThenAndElseWithMatchingMethodReturnType() " \"don't matter\"")).ParseMembersAndMethods(new MethodExpressionParser()); // @formatter:on var body = (Body)program.Methods[0].GetBodyAndParseIfNeeded(); - Assert.That(body.ReturnType.ToString(), Is.EqualTo("TestPackage.Text")); - Assert.That(body.children[0].ReturnType.ToString(), Is.EqualTo("TestPackage.Text")); - Assert.That(body.children[1].ReturnType.ToString(), Is.EqualTo("TestPackage.Text")); + Assert.That(body.ReturnType.ToString(), Is.EqualTo("TestPackage/Text")); + Assert.That(body.children[0].ReturnType.ToString(), Is.EqualTo("TestPackage/Text")); + Assert.That(body.children[1].ReturnType.ToString(), Is.EqualTo("TestPackage/Text")); } [Test] public void ParseElseIf() => - Assert.That(ParseExpression("if five is 5", "\tlogger.Log(\"Hey\")", "else if five is 5", "\tlogger.Log(\"Hey\")"), + Assert.That( + ParseExpression("if five is 5", "\tlogger.Log(\"Hey\")", "else if five is 5", + "\tlogger.Log(\"Hey\")"), Is.EqualTo(new If(GetCondition(), GetThen(), 0, new If(GetCondition(), GetThen())))); [TestCase("else if five is 6")] [TestCase("else if")] [TestCase("if five is 5", "\tlogger.Log(\"Hey\")", "else if")] public void UnexpectedElseIf(params string[] code) => - Assert.That(() => ParseExpression(code), - Throws.InstanceOf()); + Assert.That(() => ParseExpression(code), Throws.InstanceOf()); [Test] public void ElseIfWithoutThen() => @@ -189,7 +190,7 @@ public void ElseIfWithoutThen() => [Test] public void ValidMultipleElseIf() { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ValidMultipleElseIf), // @formatter:off "has logger", @@ -215,14 +216,14 @@ public void ValidMultipleElseIf() " logger.Log(\"Hello\")", " return \"Hello\"", "\"don't matter\"")), body.ToString()); - Assert.That(body.children[1].ReturnType.ToString(), Is.EqualTo("TestPackage.Text")); + Assert.That(body.children[1].ReturnType.ToString(), Is.EqualTo("TestPackage/Text")); Assert.That(body.children.Count, Is.EqualTo(3)); } [Test] public void ElseIfMissingThen() { - var program = new Type(Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ElseIfMissingThen), // @formatter:off "has logger", @@ -241,7 +242,7 @@ public void ElseIfMissingThen() [Test] public void MultiLineElseWithMismatchingReturnType() { - var program = new Type(Package, new TypeLines( + using var program = new Type(TestPackage.Instance, new TypeLines( nameof(MultiLineElseWithMismatchingReturnType), // @formatter:off "has logger", diff --git a/Strict.Expressions.Tests/IfTests.cs b/Strict.Expressions.Tests/IfTests.cs index ee5db224..2493b160 100644 --- a/Strict.Expressions.Tests/IfTests.cs +++ b/Strict.Expressions.Tests/IfTests.cs @@ -6,7 +6,7 @@ public sealed class IfTests : TestExpressions public void MissingCondition() => Assert.That(() => ParseExpression("if"), Throws.InstanceOf().With.Message. - Contains("TestPackage" + Path.DirectorySeparatorChar + "dummy.strict:line 2")); + Contains(Path.Combine("Strict", "dummy.strict") + ":line 2")); [Test] public void InvalidCondition() => @@ -28,7 +28,7 @@ public void ReturnTypeOfThenAndElseIsNumberAndCharacterIsValid() => " return Character(5)", " else", " return 5" - ]).GetBodyAndParseIfNeeded().ReturnType, Is.EqualTo(type.GetType(Base.Number))); + ]).GetBodyAndParseIfNeeded().ReturnType, Is.EqualTo(type.GetType(Type.Number))); [Test] public void ParseInvalidSpaceAfterElseIsNotAllowed() => @@ -71,7 +71,7 @@ public void ValidIsNotUsageOnDifferentType() => public void ParseMissingElseExpression() => Assert.That(() => ParseExpression("if five is 5", "\tRun", "else"), Throws.InstanceOf().With.Message. - Contains("TestPackage" + Path.DirectorySeparatorChar + "dummy.strict:line 4")); + Contains(Path.Combine("Strict", "dummy.strict") + ":line 4")); [Test] public void ReturnGetHashCode() diff --git a/Strict.Expressions.Tests/ListAdvancedTests.cs b/Strict.Expressions.Tests/ListAdvancedTests.cs index 35f501f7..b53f9ee1 100644 --- a/Strict.Expressions.Tests/ListAdvancedTests.cs +++ b/Strict.Expressions.Tests/ListAdvancedTests.cs @@ -15,7 +15,7 @@ public void ListPrefixIsNotAllowed() => Assert.That( () => { - using var _ = new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(ListPrefixIsNotAllowed), "has listOne Numbers")). ParseMembersAndMethods(parser); }, //ncrunch: no coverage @@ -25,12 +25,12 @@ public void ListPrefixIsNotAllowed() => [Test] public void ListGenericLengthAddition() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ListGenericLengthAddition), "has ones Numbers", "has twos Numbers", "AddListLength Number", "\tones.Length + twos.Length")).ParseMembersAndMethods(parser); Assert.That(program.Members[0].Name, Is.EqualTo("ones")); - var numbersListType = type.GetType(Base.List). - GetGenericImplementation(type.GetType(Base.Number)); + var numbersListType = type.GetType(Type.List). + GetGenericImplementation(type.GetType(Type.Number)); Assert.That(program.Members[0].Type, Is.EqualTo(numbersListType)); Assert.That(program.Members[1].Type, Is.EqualTo(numbersListType)); } @@ -38,7 +38,7 @@ public void ListGenericLengthAddition() [Test] public void ListAdditionWithGeneric() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(ListAdditionWithGeneric), "has elements Numbers", "Add(other Numbers) List", "\telements + other.elements")).ParseMembersAndMethods(parser); Assert.That(program.Members[0].Name, Is.EqualTo("elements")); @@ -50,17 +50,17 @@ public void ListAdditionWithGeneric() [TestCase("Add(input Character) Numbers", "NumbersCompatibleWithCharacter")] public void NumbersCompatibleWithImplementedTypes(string code, string testName) { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(testName, "has logger", code, "\t(1, 2, 3, input)")). ParseMembersAndMethods(parser); Assert.That(program.Methods[0].GetBodyAndParseIfNeeded().ReturnType, - Is.EqualTo(program.GetListImplementationType(type.GetType(Base.Number)))); + Is.EqualTo(program.GetListImplementationType(type.GetType(Type.Number)))); } [Test] public void NotOperatorInAssignment() { - using var typeWithAssignment = new Type(type.Package, + using var typeWithAssignment = new Type(TestPackage.Instance, new TypeLines(nameof(NotOperatorInAssignment), "has numbers", "NotOperator", "\tconstant result = not true", "\tresult is false")).ParseMembersAndMethods(parser); var assignment = ((Body)typeWithAssignment.Methods[0].GetBodyAndParseIfNeeded()).Expressions[0]; @@ -72,7 +72,7 @@ public void UnknownExpressionForArgumentInList() => Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(UnknownExpressionForArgumentInList), "has logger", "UnknownExpression", "\tconstant result = ((1, 2), 9gf5)")). ParseMembersAndMethods(parser); @@ -83,7 +83,7 @@ public void UnknownExpressionForArgumentInList() => [Test] public void AccessListElementsByIndex() { - using var typeWithAccess = new Type(type.Package, + using var typeWithAccess = new Type(TestPackage.Instance, new TypeLines(nameof(AccessListElementsByIndex), "has numbers", "AccessZeroIndexElement Number", "\tnumbers(0)")).ParseMembersAndMethods(parser); var expression = typeWithAccess.Methods[0].GetBodyAndParseIfNeeded(); @@ -93,7 +93,7 @@ public void AccessListElementsByIndex() [Test] public void AllowMutableListWithEmptyExpressions() { - using var typeWithMutableList = new Type(type.Package, + using var typeWithMutableList = new Type(TestPackage.Instance, new TypeLines(nameof(AllowMutableListWithEmptyExpressions), "has numbers", "CreateMutableList Numbers", "\tmutable result = Numbers", "\tfor numbers", "\t\tresult = result - value", "\tresult")).ParseMembersAndMethods(parser); @@ -101,13 +101,13 @@ public void AllowMutableListWithEmptyExpressions() Assert.That(expression.Expressions[0].ToString(), Is.EqualTo("mutable result = List(Number)")); Assert.That(((Declaration)expression.Expressions[0]).Value.ReturnType.FullName, - Is.EqualTo("TestPackage.List(Number)")); + Is.EqualTo("TestPackage/List(Number)")); } [Test] public void CreateMemberWithMutableListType() { - using var mutableTextsType = new Type(type.Package, + using var mutableTextsType = new Type(TestPackage.Instance, new TypeLines(nameof(CreateMemberWithMutableListType), "mutable mutableTexts Texts", "AddFiveToMutableList Texts", "\tmutableTexts = mutableTexts + \"5\"")). ParseMembersAndMethods(parser); @@ -120,7 +120,7 @@ public void OnlyListTypeIsAllowedAsMutableExpressionArgument() => Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(OnlyListTypeIsAllowedAsMutableExpressionArgument), "has unused Logger", "MutableWithNumber Number", "\tconstant result = Mutable(Number)", "\tresult + 1")).ParseMembersAndMethods(parser); @@ -134,7 +134,7 @@ public void CheckIfInvalidArgumentIsNotMethodOrListCall() => Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(CheckIfInvalidArgumentIsNotMethodOrListCall), "has booleans", "AccessZeroIndexElement Boolean", "\tlet firstValue = booleans(0)", "\tfirstValue(0)")).ParseMembersAndMethods(parser); @@ -146,7 +146,7 @@ public void CheckIfInvalidArgumentIsNotMethodOrListCall() => public void MultiLineListsAllowedOnlyIfLengthIsMoreThanHundred() => Assert.That(() => { - using var dummy = new Type(type.Package, new TypeLines( + using var dummy = new Type(TestPackage.Instance, new TypeLines( nameof(MultiLineListsAllowedOnlyIfLengthIsMoreThanHundred), // @formatter:off "has logger", @@ -171,7 +171,7 @@ public void MultiLineListsAllowedOnlyIfLengthIsMoreThanHundred() => public void UnterminatedMultiLineListFound() => Assert.That(() => { - using var dummy = new Type(type.Package, new TypeLines(nameof(UnterminatedMultiLineListFound), + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(UnterminatedMultiLineListFound), // @formatter:off "has logger", "Run", @@ -235,7 +235,7 @@ public void ParseMultiLineExpressionAndPrintSameAsInput(string testName, string params string[] code) { using var program = - new Type(type.Package, new TypeLines(testName, code)).ParseMembersAndMethods(parser); + new Type(TestPackage.Instance, new TypeLines(testName, code)).ParseMembersAndMethods(parser); var expression = program.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(expression, Is.InstanceOf()); Assert.That(expression.ToString(), Is.EqualTo(expected)); @@ -245,7 +245,7 @@ public void ParseMultiLineExpressionAndPrintSameAsInput(string testName, string [Test] public void MergeFromConstructorParametersIntoListIfMemberMatches() { - using var program = new Type(type.Package, new TypeLines( + using var program = new Type(TestPackage.Instance, new TypeLines( // @formatter:off "Vector2", "has numbers with Length is 2", @@ -255,14 +255,14 @@ public void MergeFromConstructorParametersIntoListIfMemberMatches() "\tVector2(3, 4).Length is 5", "\t(X * X + Y * Y).SquareRoot")).ParseMembersAndMethods(parser); Assert.That(program.Members[1].Name, Is.EqualTo("One")); - Assert.That(program.Members[1].Type.ToString(), Is.EqualTo("TestPackage.Vector2")); + Assert.That(program.Members[1].Type.ToString(), Is.EqualTo("TestPackage/Vector2")); } [Test] public void FromConstructorCannotBeCreatedWhenFirstMemberIsNotMatched() => Assert.That(() => { - using var _ = new Type(type.Package, new TypeLines( + using var _ = new Type(TestPackage.Instance, new TypeLines( "CannotCreateFromConstructor", "constant One = CannotCreateFromConstructor(1)", "has numbers with Length is 2", @@ -280,7 +280,7 @@ public void AutoParseArgumentAsListIfMatchingWithMethodParameter(string paramete string arguments, string expectedList) { // @formatter:off - using var typeWithTestMethods = new Type(type.Package, + using var typeWithTestMethods = new Type(TestPackage.Instance, new TypeLines("ListArgumentsCanBeAutoParsed" + parameter, "has logger", $"CheckInputLengthAndGetResult({parameter}) Number", @@ -297,7 +297,7 @@ public void AutoParseArgumentAsListIfMatchingWithMethodParameter(string paramete var argumentExpression = ((MethodCall)body.Expressions[1]).Arguments[0]; Assert.That(argumentExpression, Is.InstanceOf()); Assert.That(argumentExpression.ReturnType.ToString(), - Is.EqualTo("TestPackage." + expectedList)); + Is.EqualTo("TestPackage/" + expectedList)); } [Test] @@ -335,7 +335,7 @@ public void ChangeValueInsideMutableListWithMutableExpressions() [Test] public void NegativeIndexIsNeverAllowed() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(NegativeIndexIsNeverAllowed), "has logger", "UpdateNotExistingElement(element Number) Number", @@ -348,7 +348,7 @@ public void NegativeIndexIsNeverAllowed() [Test] public void UpdateListExpressionValuesByIndex() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(UpdateListExpressionValuesByIndex), "has logger", "UpdateListValue(element Number) Number", @@ -363,7 +363,7 @@ public void UpdateListExpressionValuesByIndex() [Test] public void IndexAboveConstantListLength() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(IndexAboveConstantListLength), "has logger", "UpdateNotExistingElement(element Number) Number", @@ -376,7 +376,7 @@ public void IndexAboveConstantListLength() [Test] public void IndexViolatesListConstraint() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(IndexViolatesListConstraint), "has numbers with Length is 2", "from", @@ -388,7 +388,7 @@ public void IndexViolatesListConstraint() [Test] public void IndexCheckEvenWorkWhenIndexIsConstant() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(IndexCheckEvenWorkWhenIndexIsConstant), "has numbers with Length is 2", "from", @@ -401,7 +401,7 @@ public void IndexCheckEvenWorkWhenIndexIsConstant() [Test] public void IndexCheckAlsoWorksForMemberCalls() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(IndexCheckAlsoWorksForMemberCalls), "has numbers with Length is 2", "constant invalidIndex = 3", @@ -414,7 +414,7 @@ public void IndexCheckAlsoWorksForMemberCalls() [Test] public void IndexCannotBeCheckedOnADynamicCall() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(IndexCannotBeCheckedOnADynamicCall), "has numbers with Length is 2", "from(number)", diff --git a/Strict.Expressions.Tests/ListTests.cs b/Strict.Expressions.Tests/ListTests.cs index b01644ce..f714b6d4 100644 --- a/Strict.Expressions.Tests/ListTests.cs +++ b/Strict.Expressions.Tests/ListTests.cs @@ -30,9 +30,9 @@ public void ParseLists(string input, string expected) => ParseAndCheckOutputMatchesInput(input, new List(new Body(method), GetListExpressions(expected.Split(", ")))); - private List GetListExpressions(IEnumerable elements) + private List GetListExpressions(IReadOnlyList elements) { - var expressions = new List(); + var expressions = new List(elements.Count); var body = new Body(method); foreach (var elementWithSpace in elements) AddElementExpression(expressions, elementWithSpace.TrimStart(), body); @@ -102,7 +102,7 @@ public void LeftTypeShouldNotBeChanged() Assert.That(parsedExpression, Is.InstanceOf()); Assert.That(parsedExpression.ToString(), Is.EqualTo(Code)); Assert.That(((Binary)parsedExpression).ReturnType, - Is.EqualTo(type.GetType(Base.Text.Pluralize()))); + Is.EqualTo(type.GetType(Type.Text.Pluralize()))); } [Test] @@ -120,9 +120,9 @@ public void ConstructorForSameTypeArgumentIsNotAllowed(string code) => Assert.That(() => ParseExpression(code), Throws.InstanceOf()); - [TestCase("(1, 2)", Base.Number)] - [TestCase("(\"1\", \"2\")", Base.Text)] - [TestCase("(true, false)", Base.Boolean)] + [TestCase("(1, 2)", Type.Number)] + [TestCase("(\"1\", \"2\")", Type.Text)] + [TestCase("(true, false)", Type.Boolean)] public void ListShouldHaveCorrectImplementationReturnType(string code, string expectedType) => Assert.That(ParseExpression(code).ReturnType, Is.EqualTo(type.GetListImplementationType(type.GetType(expectedType)))); @@ -130,10 +130,11 @@ public void ListShouldHaveCorrectImplementationReturnType(string code, string ex [Test] public void ParseMultipleListInBinary() => ParseAndCheckOutputMatchesInput("(1, 2, 3, 4, 5) + (1) + 4", - CreateBinary(new List(new Body(method), GetListExpressions("1, 2, 3, 4, 5".Split(", "))), - BinaryOperator.Plus, - CreateBinary(new List(new Body(method), GetListExpressions("1".Split(", "))), - BinaryOperator.Plus, new Number(method, 4)))); + CreateBinary( + CreateBinary(new List(new Body(method), GetListExpressions("1, 2, 3, 4, 5".Split(", "))), + BinaryOperator.Plus, + new List(new Body(method), GetListExpressions("1".Split(", ")))), + BinaryOperator.Plus, new Number(method, 4))); [Test] public void ParseNestedLists() => @@ -168,36 +169,51 @@ public void ContainsMethodCallOnNumbersList() [Test] public void MethodsAndMembersOfListShouldHaveImplementationTypeAsParent() { - var numbers = type.GetListImplementationType(type.GetType(Base.Number)); + var numbers = type.GetListImplementationType(type.GetType(Type.Number)); Assert.That(numbers.Members[1].ToString(), - Is.EqualTo("elements TestPackage.List(Number)")); + Is.EqualTo("elements TestPackage/List(Number)")); Assert.That(numbers.Methods[1].Parent.ToString(), - Is.EqualTo("TestPackage.List(Number)")); + Is.EqualTo("TestPackage/List(Number)")); } [Test] public void MethodBodyShouldBeUpdatedWithImplementationType() { - var texts = type.GetListImplementationType(type.GetType(Base.Text)); + var texts = type.GetListImplementationType(type.GetType(Type.Text)); var containsMethod = texts.Methods.FirstOrDefault(m => - m.Name == BinaryOperator.In && m.Parameters[0].Type.Name == Base.Text); + m.Name == BinaryOperator.In && m.Parameters[0].Type.IsText); Assert.That(containsMethod!.Type, Is.EqualTo(texts)); - Assert.That(containsMethod.Parameters[0].Type.Name, Is.EqualTo(Base.Text)); + Assert.That(containsMethod.Parameters[0].Type.Name, Is.EqualTo(Type.Text)); var body = (Body)containsMethod.GetBodyAndParseIfNeeded(); Assert.That(body.Method, Is.EqualTo(containsMethod)); Assert.That(body.Method.Type, Is.EqualTo(texts)); - Assert.That(body.Method.Parameters[0].Type.Name, Is.EqualTo(Base.Text)); - Assert.That(body.Method, Is.EqualTo(containsMethod), texts.Methods.ToWordList()); + Assert.That(body.Method.Parameters[0].Type.Name, Is.EqualTo(Type.Text)); + Assert.That(body.Method, Is.EqualTo(containsMethod), string.Join(", ", texts.Methods)); } [Test] public void CompareLists() => Assert.That( - EqualsExtensions.AreEqual(new List { new(type, 2), new(type, 3) }, + new List { new(type, 2), new(type, 3) }.SequenceEqual( new List { new(type, 2), new(type, 3) }), Is.True); [Test] public void Index() => Assert.That(ParseExpression("(1, 2, 3).Index(9) is -1").ReturnType, - Is.EqualTo(type.GetType(Base.Boolean))); + Is.EqualTo(type.GetType(Type.Boolean))); + + [Test] + public void MutableListIsNotConstant() + { + var body = (Body)ParseExpression("mutable numbers = (1, 2, 3)", "numbers.Add(4)"); + Assert.That(body.Expressions[^1].IsConstant, Is.False); + } + + [Test] + public void CannotGetConstantDataFromListWithNonConstants() + { + var list = (List)ParseExpression("(1, 2, 3, 4, five)"); + Assert.That(() => list.Data, Throws.InstanceOf()); + Assert.That(list.TryGetConstantData(), Is.Null); + } } \ No newline at end of file diff --git a/Strict.Expressions.Tests/LoggerTests.cs b/Strict.Expressions.Tests/LoggerTests.cs index ed344beb..c5d2a504 100644 --- a/Strict.Expressions.Tests/LoggerTests.cs +++ b/Strict.Expressions.Tests/LoggerTests.cs @@ -7,16 +7,18 @@ public sealed class LoggerTests [Test] public void PrintHelloWorld() { - new Type(TestPackage.Instance, new TypeLines(Base.App, "Run")); - var type = - new Type(TestPackage.Instance, - new TypeLines("Program", "has App", "has logger", "Run", "\tlogger.Log(\"Hello\")")). - ParseMembersAndMethods(new MethodExpressionParser()); + using var app = new Type(TestPackage.Instance, new TypeLines(Type.App, "Run")); + using var type = new Type(TestPackage.Instance, + new TypeLines(nameof(PrintHelloWorld), "has App", "has logger", "Run", + "\tlogger.Log(\"Hello\")")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(Run(type.Methods[0]), Is.EqualTo("Hello")); } - public string Run(Method method) => - method.GetBodyAndParseIfNeeded() is MethodCall call && call.Method.Name == "Log" - ? ((Text)call.Arguments[0]).Data.ToString()! - : ""; + public string Run(Method method) + { + if (method.GetBodyAndParseIfNeeded() is not MethodCall call || call.Method.Name != "Log") + return ""; //ncrunch: no coverage + var text = (Text)call.Arguments[0]; + return text.Data.ToExpressionCodeString(); + } } \ No newline at end of file diff --git a/Strict.Expressions.Tests/MemberCallTests.cs b/Strict.Expressions.Tests/MemberCallTests.cs index e3578b46..fb27cdde 100644 --- a/Strict.Expressions.Tests/MemberCallTests.cs +++ b/Strict.Expressions.Tests/MemberCallTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public sealed class MemberCallTests : TestExpressions @@ -19,7 +21,7 @@ public void MembersMustBeWords() => public void NestedMemberNotFound() => Assert.That(() => ParseExpression("logger.unknown"), Throws.InstanceOf().With.Message. - StartsWith("unknown in TestPackage.Log")); + StartsWith("unknown in TestPackage/Log")); [Test] public void NumbersCanNotStartNestedCall() => @@ -46,12 +48,10 @@ public void ValidMemberCall() => [Test] public void MemberWithArgumentsInitializerShouldNotHaveType() => - Assert.That( - () => + Assert.That(() => { - using var _ = - new Type(type.Package, new TypeLines("Declaration", "has input Text = Text(5)")). - ParseMembersAndMethods(parser); + using var _ = new Type(TestPackage.Instance, new TypeLines(Body.Declaration, + "has input Text = Text(5)")).ParseMembersAndMethods(parser); }, //ncrunch: no coverage Throws.InstanceOf().With.InnerException. InstanceOf()); @@ -63,7 +63,7 @@ public void UnknownExpressionInMemberInitializer() => Assert.That( () => { - using var _ = new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(UnknownExpressionInMemberInitializer), "has input Text = random")). ParseMembersAndMethods(parser); @@ -75,7 +75,7 @@ public void NameMustBeAWordWithoutAnySpecialCharacterOrNumber() => Assert.That( () => { - using var _ = new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(NameMustBeAWordWithoutAnySpecialCharacterOrNumber), "has input1$ = Text(5)")).ParseMembersAndMethods(parser); }, //ncrunch: no coverage @@ -85,25 +85,24 @@ public void NameMustBeAWordWithoutAnySpecialCharacterOrNumber() => [Test] public void MemberWithArgumentsInitializer() { - using var assignmentType = new Type(type.Package, + using var assignmentType = new Type(TestPackage.Instance, new TypeLines(nameof(MemberWithArgumentsInitializer), "has input = Character(5)", "GetInput Text", "\tinput")).ParseMembersAndMethods(parser); Assert.That(assignmentType.Members[0].Name, Is.EqualTo("input")); Assert.That(assignmentType.Members[0].IsPublic, Is.False); - Assert.That(assignmentType.Members[0].Type, Is.EqualTo(type.GetType(Base.Character))); + Assert.That(assignmentType.Members[0].Type, Is.EqualTo(type.GetType(Type.Character))); Assert.That(assignmentType.Members[0].InitialValue, Is.InstanceOf()); var methodCall = (MethodCall)assignmentType.Members[0].InitialValue!; - Assert.That(methodCall.Method.ReturnType.Name, Is.EqualTo(Base.Character)); + Assert.That(methodCall.Method.ReturnType.Name, Is.EqualTo(Type.Character)); Assert.That(methodCall.Arguments[0], Is.EqualTo(new Number(type, 5))); } [Test] public void MemberGetHashCodeAndEquals() { - using var memberCall = - new Type(type.Package, - new TypeLines(nameof(MemberGetHashCodeAndEquals), "has input = Text(5)", "GetInput Text", - "\tinput")).ParseMembersAndMethods(parser); + using var memberCall = new Type(TestPackage.Instance, + new TypeLines(nameof(MemberGetHashCodeAndEquals), "has input = Text(5)", "GetInput Text", + "\tinput")).ParseMembersAndMethods(parser); Assert.That(memberCall.Members[0].GetHashCode(), Is.EqualTo(memberCall.Members[0].Name.GetHashCode())); Assert.That( @@ -118,15 +117,13 @@ public void MemberGetHashCodeAndEquals() public void MemberWithBinaryExpression() { // @formatter:off - using var assignmentType = - new Type(type.Package, - new TypeLines(nameof(MemberWithBinaryExpression), - "has combinedNumber = 3 + 5", - "GetCombined Number", - "\tcombinedNumber")). - ParseMembersAndMethods(parser); + using var assignmentType = new Type(TestPackage.Instance, + new TypeLines(nameof(MemberWithBinaryExpression), + "has combinedNumber = 3 + 5", + "GetCombined Number", + "\tcombinedNumber")).ParseMembersAndMethods(parser); Assert.That(assignmentType.Members[0].Name, Is.EqualTo("combinedNumber")); - Assert.That(assignmentType.Members[0].Type, Is.EqualTo(type.GetType(Base.Number))); + Assert.That(assignmentType.Members[0].Type, Is.EqualTo(type.GetType(Type.Number))); var binary = (Binary)assignmentType.Members[0].InitialValue!; Assert.That(binary.Instance, Is.EqualTo(new Number(type, 3))); Assert.That(binary.Arguments[0], Is.EqualTo(new Number(type, 5))); @@ -135,7 +132,7 @@ public void MemberWithBinaryExpression() [Test] public void FromConstructorCall() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(FromConstructorCall), "has file = File(\"test.txt\")", "Run", @@ -147,7 +144,7 @@ public void FromConstructorCall() [Test] public void FromConstructorCallUsingMemberName() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(FromConstructorCallUsingMemberName), "has file = \"test.txt\"", "Run", @@ -160,7 +157,7 @@ public void MemberCallUsingAnotherMemberIsForbidden() => Assert.That( () => { - using var _ = new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(MemberCallUsingAnotherMemberIsForbidden), "has file = File(\"test.txt\")", "has fileDescription = file.Length > 1000 then \"big file\" else \"small file\"", @@ -172,7 +169,7 @@ public void MemberCallUsingAnotherMemberIsForbidden() => [Test] public void BaseTypeMemberCallInDerivedType() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(BaseTypeMemberCallInDerivedType), "has Range", "Run", @@ -190,7 +187,7 @@ public void DuplicateMembersAreNotAllowed() => Assert.That( () => { - using var _ = new Type(type.Package, new TypeLines(nameof(DuplicateMembersAreNotAllowed), + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(DuplicateMembersAreNotAllowed), "has something Number", "has something Number", "Run", @@ -201,7 +198,7 @@ public void DuplicateMembersAreNotAllowed() => [Test] public void MembersWithDifferentNamesAreAllowed() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MembersWithDifferentNamesAreAllowed), "has something Number", "has somethingDifferent Number", @@ -216,7 +213,7 @@ public void MemberNameWithDifferentTypeNamesThanOwnNotAllowed() => Assert.That( () => { - using var _ = new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(MemberNameWithDifferentTypeNamesThanOwnNotAllowed), "has numbers Boolean", "Run", @@ -230,7 +227,7 @@ public void VariableNameCannotHaveDifferentTypeNameThanValue() => Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(VariableNameCannotHaveDifferentTypeNameThanValue), "has text", "Run", @@ -246,7 +243,7 @@ public void CannotAccessMemberInSameTypeBeforeTypeIsParsed() => Assert.That( () => { - using var _ = new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(CannotAccessMemberInSameTypeBeforeTypeIsParsed), "has Range", "has something = Range(0, 13)", diff --git a/Strict.Expressions.Tests/MethodCallTests.cs b/Strict.Expressions.Tests/MethodCallTests.cs index d3b3ad40..ff693e09 100644 --- a/Strict.Expressions.Tests/MethodCallTests.cs +++ b/Strict.Expressions.Tests/MethodCallTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public sealed class MethodCallTests : TestExpressions @@ -118,7 +120,7 @@ public void ArgumentsDoNotMatchMethodParameters() => public void ParseCallWithUnknownMemberCallArgument() => Assert.That(() => ParseExpression("logger.Log(logger.unknown)"), Throws.InstanceOf().With.Message. - StartsWith("unknown in TestPackage.Logger")); + StartsWith("unknown in TestPackage/Logger")); [Test] public void MethodCallMembersMustBeWords() => @@ -139,7 +141,7 @@ public void ListTokensAreNotSeparatedByCommaException() => [Test] public void SimpleFromMethodCall() => Assert.That(ParseExpression("Character(7)"), - Is.EqualTo(CreateFromMethodCall(type.GetType(Base.Character), new Number(type, 7)))); + Is.EqualTo(CreateFromMethodCall(type.GetType(Type.Character), new Number(type, 7)))); [TestCase("Character(5)")] [TestCase("Range(0, 10)")] @@ -152,7 +154,7 @@ public void MakeSureMutableTypeMethodsAreNotModified() { var body = (Body)ParseExpression("mutable variable = 7", "variable = variable + 1"); var expression = body.Expressions[0]; - Assert.That(type.GetType(Base.Mutable).Methods.Count, Is.EqualTo(0)); + Assert.That(type.GetType(Type.Mutable).Methods.Count, Is.EqualTo(0)); Assert.That(expression is Declaration, Is.True); Assert.That(((Declaration)expression).IsMutable, Is.True); } @@ -177,14 +179,14 @@ public void IsMethodPublic() => [Test] public void ValueMustHaveCorrectType() { - var program = new Type(type.Package, new TypeLines( - nameof(ValueMustHaveCorrectType), - "has logger", - "has Number", - $"Dummy(dummy Number) {nameof(ValueMustHaveCorrectType)}", - "\tlet result = value", - "\tresult is " + nameof(ValueMustHaveCorrectType), - "\tresult")). + using var program = new Type(TestPackage.Instance, + new TypeLines(nameof(ValueMustHaveCorrectType), + "has logger", + "has Number", + $"Dummy(dummy Number) {nameof(ValueMustHaveCorrectType)}", + "\tlet result = value", + "\tresult is " + nameof(ValueMustHaveCorrectType), + "\tresult")). ParseMembersAndMethods(new MethodExpressionParser()); Assert.That( ((Body)program.Methods[0].GetBodyAndParseIfNeeded()).FindVariable("value")?.Type, @@ -194,15 +196,14 @@ public void ValueMustHaveCorrectType() [Test] public void CanAccessThePropertiesOfValue() { - var program = new Type(type.Package, new TypeLines( - nameof(CanAccessThePropertiesOfValue), + using var program = new Type(TestPackage.Instance, + new TypeLines(nameof(CanAccessThePropertiesOfValue), "has logger", "has Number", "has myMember Text", "Dummy(dummy Number) Text", "\tlet result = value.myMember", - "\tresult + \"dummy\"")). - ParseMembersAndMethods(new MethodExpressionParser()); + "\tresult + \"dummy\"")).ParseMembersAndMethods(new MethodExpressionParser()); var body = (Body)program.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(body.FindVariable("value")?.Type, Is.EqualTo(program)); Assert.That(body.FindVariable("result")?.Type.Name, Is.EqualTo("Text")); @@ -220,9 +221,7 @@ public void CanAccessThePropertiesOfValue() "\tinstanceWithTexts is ProgramWithPublicMember")] public void ParseConstructorCallWithList(string programName, string expected, params string[] code) { - var program = new Type(type.Package, new TypeLines( - programName, - code)). + using var program = new Type(TestPackage.Instance, new TypeLines(programName, code)). ParseMembersAndMethods(new MethodExpressionParser()); var assignment = (Declaration)((Body)program.Methods[0].GetBodyAndParseIfNeeded()).Expressions[0]; @@ -232,7 +231,7 @@ public void ParseConstructorCallWithList(string programName, string expected, pa [Test] public void TypeImplementsGenericTypeWithLength() { - new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines("HasLengthImplementation", "has HasLength", "has boolean", @@ -240,7 +239,7 @@ public void TypeImplementsGenericTypeWithLength() "\tboolean = boolean", "Length Number", "\tvalue")).ParseMembersAndMethods(new MethodExpressionParser()); - var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(TypeImplementsGenericTypeWithLength), "has logger", "GetLengthSquare(type HasLength) Number", "\ttype.Length * type.Length", "Dummy", "\tconstant countOfFive = HasLengthImplementation(true)", @@ -254,7 +253,7 @@ public void TypeImplementsGenericTypeWithLength() [Test] public void MutableCanUseChildMethods() { - var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MutableCanUseChildMethods), "has logger", "Dummy Number", @@ -267,7 +266,7 @@ public void MutableCanUseChildMethods() [Test] public void ConstructorCallWithMethodCall() { - var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines("ArithmeticFunction", "has numbers", "from(first Number, second Number)", @@ -281,7 +280,7 @@ public void ConstructorCallWithMethodCall() [Test] public void RecursiveStackOverflow() { - var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines("RecursiveStackOverflow", "has number", "AddFiveWithInput Number", @@ -295,14 +294,15 @@ public void RecursiveStackOverflow() [Test] public void NestedMethodCall() { - var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(NestedMethodCall), "has logger", "Run", "\tFile(\"fileName\").Write(\"someText\")", "\ttrue")). ParseMembersAndMethods(new MethodExpressionParser()); var body = (Body)program.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(body.Expressions[0], Is.InstanceOf()); Assert.That(((MethodCall)body.Expressions[0]).Method.Name, Is.EqualTo("Write")); - Assert.That(((MethodCall)body.Expressions[0]).ToString(), Is.EqualTo("File(\"fileName\").Write(\"someText\")")); + Assert.That(((MethodCall)body.Expressions[0]).ToString(), + Is.EqualTo("File(\"fileName\").Write(\"someText\")")); Assert.That(((MethodCall)body.Expressions[0]).Instance?.ToString(), Is.EqualTo("File(\"fileName\")")); } @@ -310,13 +310,12 @@ public void NestedMethodCall() [Test] public void MethodCallAsMethodParameter() { - var program = new Type(type.Package, - new TypeLines(nameof(MethodCallAsMethodParameter), - "has logger", - "AppendFiveWithInput(number) Number", - "\tAppendFiveWithInput(AppendFiveWithInput(5)) is 15", - "\tnumber + 5")). - ParseMembersAndMethods(new MethodExpressionParser()); + using var program = new Type(TestPackage.Instance, + new TypeLines(nameof(MethodCallAsMethodParameter), + "has logger", + "AppendFiveWithInput(number) Number", + "\tAppendFiveWithInput(AppendFiveWithInput(5)) is 15", + "\tnumber + 5")).ParseMembersAndMethods(new MethodExpressionParser()); var body = (Body)program.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(body.Expressions[0].ToString(), Is.EqualTo("AppendFiveWithInput(AppendFiveWithInput(5)) is 15")); @@ -327,14 +326,13 @@ public void MethodCallAsMethodParameter() [Test] public void TypeCanBeAutoInitialized() { - new Type(type.Package, - new TypeLines(nameof(TypeCanBeAutoInitialized), - "has logger", - "AddFiveWithInput(number) Number", - "\tAddFiveWithInput(AddFiveWithInput(5)) is 15", - "\tnumber + 5")). - ParseMembersAndMethods(new MethodExpressionParser()); - var consumingType = new Type(type.Package, + using var _ = new Type(TestPackage.Instance, + new TypeLines(nameof(TypeCanBeAutoInitialized), + "has logger", + "AddFiveWithInput(number) Number", + "\tAddFiveWithInput(AddFiveWithInput(5)) is 15", + "\tnumber + 5")).ParseMembersAndMethods(new MethodExpressionParser()); + using var consumingType = new Type(TestPackage.Instance, new TypeLines("AutoInitializedTypeConsumer", "has typeCanBeAutoInitialized", "GetResult(number) Number", @@ -343,20 +341,21 @@ public void TypeCanBeAutoInitialized() ParseMembersAndMethods(new MethodExpressionParser()); var body = (Body)consumingType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(consumingType.Members[0].Type.Name, Is.EqualTo("TypeCanBeAutoInitialized")); - Assert.That(((MethodCall)body.Expressions[1]).Instance?.ReturnType.Name, Is.EqualTo("TypeCanBeAutoInitialized")); + Assert.That(((MethodCall)body.Expressions[1]).Instance?.ReturnType.Name, + Is.EqualTo("TypeCanBeAutoInitialized")); } [Test] public void TypeCannotBeAutoInitialized() { - new Type(type.Package, + using var _ = new Type(TestPackage.Instance, new TypeLines(nameof(TypeCannotBeAutoInitialized), "has number", "AddFiveWithInput Number", "\tTypeCannotBeAutoInitialized(10).AddFiveWithInput is 15", "\tnumber + 5")). ParseMembersAndMethods(new MethodExpressionParser()); - var consumer = new Type(type.Package, + using var consumer = new Type(TestPackage.Instance, new TypeLines("ConsumingType", "has logger", "GetResult(number) Number", @@ -365,6 +364,7 @@ public void TypeCannotBeAutoInitialized() "\tinstance.AddFiveWithInput")). ParseMembersAndMethods(new MethodExpressionParser()); var body = (Body)consumer.Methods[0].GetBodyAndParseIfNeeded(); - Assert.That(((Declaration)body.Expressions[1]).Value.ReturnType.Name, Is.EqualTo("TypeCannotBeAutoInitialized")); + Assert.That(((Declaration)body.Expressions[1]).Value.ReturnType.Name, + Is.EqualTo("TypeCannotBeAutoInitialized")); } } \ No newline at end of file diff --git a/Strict.Expressions.Tests/MethodExpressionParserTests.cs b/Strict.Expressions.Tests/MethodExpressionParserTests.cs index 453cca4a..6cc8f635 100644 --- a/Strict.Expressions.Tests/MethodExpressionParserTests.cs +++ b/Strict.Expressions.Tests/MethodExpressionParserTests.cs @@ -12,12 +12,11 @@ public void CannotParseEmptyInputException() => [Test] public void ParseSingleLine() { - var body = - (Body)new Method(type, 0, this, - [MethodTests.Run, MethodTests.ConstantNumber, "\tnumber is Number"]). - GetBodyAndParseIfNeeded(); + var body = (Body)new Method(type, 0, this, + [MethodTests.Run, MethodTests.ConstantNumber, "\tnumber is Number"]). + GetBodyAndParseIfNeeded(); var declaration = (Declaration)body.Expressions[0]; - Assert.That(declaration.ReturnType, Is.EqualTo(type.FindType(Base.Number))); + Assert.That(declaration.ReturnType, Is.EqualTo(type.FindType(Type.Number))); Assert.That(declaration.ToString(), Is.EqualTo(MethodTests.ConstantNumber[1..])); } @@ -55,9 +54,9 @@ public void ParseErrorExpression(string errorExpression) var body = (Body)new Method(type, 0, this, [ MethodTests.Run, MethodTests.ConstantErrorMessage, errorExpression ]).GetBodyAndParseIfNeeded(); - Assert.That(body.ReturnType, Is.EqualTo(type.FindType(Base.None))); + Assert.That(body.ReturnType, Is.EqualTo(type.FindType(Type.None))); Assert.That(body.Expressions, Has.Count.EqualTo(2)); - Assert.That(body.Expressions[0].ReturnType.Name, Is.EqualTo(Base.Text)); + Assert.That(body.Expressions[0].ReturnType.Name, Is.EqualTo(Type.Text)); Assert.That(body.Expressions[1], Is.TypeOf()); } diff --git a/Strict.Expressions.Tests/MutableReassignmentTests.cs b/Strict.Expressions.Tests/MutableReassignmentTests.cs index 011ed86b..7c3d63fb 100644 --- a/Strict.Expressions.Tests/MutableReassignmentTests.cs +++ b/Strict.Expressions.Tests/MutableReassignmentTests.cs @@ -1,3 +1,5 @@ +using Strict.Language.Tests; + namespace Strict.Expressions.Tests; public sealed class MutableReassignmentTests : TestExpressions @@ -10,44 +12,44 @@ public sealed class MutableReassignmentTests : TestExpressions [Test] public void MutableMemberConstructorWithType() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MutableMemberConstructorWithType), "mutable something Number", "Add(input Number) Number", "\tsomething + input")); program.ParseMembersAndMethods(parser); Assert.That(program.Members[0].IsMutable, Is.True); Assert.That(program.Methods[0].GetBodyAndParseIfNeeded().ReturnType, - Is.EqualTo(type.GetType(Base.Number))); + Is.EqualTo(type.GetType(Type.Number))); } [Test] public void MutableMethodParameterWithType() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MutableMethodParameterWithType), "has something Number", "Add(mutable input Number, mutable text) Number", "\tinput = something + input")); program.ParseMembersAndMethods(parser); Assert.That(program.Methods[0].Parameters[0].IsMutable, Is.True); Assert.That(program.Methods[0].Parameters[1].IsMutable, Is.True); Assert.That(program.Methods[0].GetBodyAndParseIfNeeded().ReturnType, - Is.EqualTo(type.GetType(Base.Number))); + Is.EqualTo(type.GetType(Type.Number))); } [Test] public void MutableListType() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MutableListType), "has numbers", "Run Mutable(List(Number))", "\tnumbers.Add(5)")); program.ParseMembersAndMethods(parser); Assert.That(program.Methods[0].GetBodyAndParseIfNeeded().ReturnType, - Is.EqualTo(type.GetType(Base.Mutable). - GetGenericImplementation(type.GetListImplementationType(type.GetType(Base.Number))))); + Is.EqualTo(type.GetType(Type.Mutable). + GetGenericImplementation(type.GetListImplementationType(type.GetType(Type.Number))))); } [Test] public void EnsureMutableMethodParameterValueIsUpdated() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(EnsureMutableMethodParameterValueIsUpdated), "has something Number", "Add(mutable input Number) Number", "\tinput = something + input")); program.ParseMembersAndMethods(parser); @@ -61,7 +63,7 @@ public void IncompleteMutableMethodParameter() => Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(IncompleteMutableMethodParameter), "has something Number", "Add(mutable input) Number", "\tinput = something + input")); dummy.ParseMembersAndMethods(parser); @@ -71,7 +73,7 @@ public void IncompleteMutableMethodParameter() => [Test] public void MutableMemberWithTextType() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MutableMemberWithTextType), "mutable something Text", "Add(input Number) Text", "\tconstant result = input + something")); program.ParseMembersAndMethods(parser); @@ -83,7 +85,7 @@ public void MutableMemberWithTextType() [Test] public void MutableVariablesUsingSameValueTypeMustBeEqual() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MutableVariablesUsingSameValueTypeMustBeEqual), "has unused Number", "UnusedMethod Number", "\tmutable first = 5", @@ -105,7 +107,7 @@ public void ValueTypeNotMatchingWithAssignmentType(string testName, params strin Assert.That( () => { - using var dummyType = new Type(type.Package, new TypeLines(testName, code)); + using var dummyType = new Type(TestPackage.Instance, new TypeLines(testName, code)); dummyType.ParseMembersAndMethods(parser).Methods[0].GetBodyAndParseIfNeeded(); }, //ncrunch: no coverage Throws.InstanceOf()); @@ -113,7 +115,7 @@ public void ValueTypeNotMatchingWithAssignmentType(string testName, params strin [Test] public void MutableVariableInstanceUsingSpace() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MutableVariableInstanceUsingSpace), "has logger", "Add(input Number) Number", "\tmutable result = 5", "\tresult = result + input")); program.ParseMembersAndMethods(parser); @@ -126,7 +128,7 @@ public void MissingMutableArgument() => Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(MissingMutableArgument), "has logger", "Add(input Number) Number", "\tconstant result =", "\tresult = result + input")); dummy.ParseMembersAndMethods(parser).Methods[0].GetBodyAndParseIfNeeded(); @@ -137,7 +139,7 @@ public void MissingMutableArgument() => [TestCase("Range(1, 10).Start", "Number", "MutableTypeWithNestedCallShouldUseBrackets")] public void MutableTypeWithListArgumentIsAllowed(string code, string returnType, string testName) { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(testName, "has logger", $"Add(input Number) {returnType}", $"\tmutable result = {code}", "\tresult = result + input")); program.ParseMembersAndMethods(parser); @@ -150,14 +152,14 @@ public void MutableTypeWithListArgumentIsAllowed(string code, string returnType, [Test] public void AssignmentWithMutableKeyword() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(AssignmentWithMutableKeyword), "has something Character", "CountEvenNumbers(limit Number) Number", "\tmutable counter = 0", "\tfor Range(0, limit)", "\t\tcounter = counter + 1", "\tcounter")); program.ParseMembersAndMethods(parser); var body = (Body)program.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(body.ReturnType, - Is.EqualTo(type.GetType(Base.Number))); + Is.EqualTo(type.GetType(Type.Number))); Assert.That(body.Expressions[0].ReturnType.Name, Is.EqualTo("Number")); } @@ -165,7 +167,7 @@ public void AssignmentWithMutableKeyword() [Test] public void MissingAssignmentValueExpression() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MissingAssignmentValueExpression), "has something Character", "CountEvenNumbers(limit Number) Number", "\tmutable counter =", "\tcounter")); program.ParseMembersAndMethods(parser); @@ -176,7 +178,7 @@ public void MissingAssignmentValueExpression() [Test] public void DirectUsageOfMutableTypesOrImplementsAreForbidden() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(DirectUsageOfMutableTypesOrImplementsAreForbidden), "has unused Character", "DummyCount(limit Number) Number", "\tconstant result = Mutable(5)", "\tresult + 1")); @@ -188,7 +190,7 @@ public void DirectUsageOfMutableTypesOrImplementsAreForbidden() [Test] public void GenericTypesCannotBeUsedDirectlyUseImplementation() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(GenericTypesCannotBeUsedDirectlyUseImplementation), "has unused Character", "DummyCount Number", "\tconstant result = List", "\tresult(0)")); @@ -201,7 +203,7 @@ public void GenericTypesCannotBeUsedDirectlyUseImplementation() [Test] public void MemberDeclarationUsingMutableKeyword() { - using var program = new Type(type.Package, + using var program = new Type(TestPackage.Instance, new TypeLines(nameof(MemberDeclarationUsingMutableKeyword), "mutable input = 0", "DummyAssignment(limit Number) Number", "\tif limit > 5", "\t\tinput = 5", "\telse", "\t\tinput = 10", "\tinput")); @@ -218,7 +220,7 @@ public void MutableTypesUsageInMembersAreForbidden(string testName, string code) Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(testName + nameof(MutableTypesUsageInMembersAreForbidden), $"mutable something {code}", "Add(input Count) Number", "\tconstant result = something + input")); @@ -229,13 +231,13 @@ public void MutableTypesUsageInMembersAreForbidden(string testName, string code) [Test] public void CannotReassignValuesToImmutableMember() { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines("BaseClever", "mutable Number", "Compute Number", "\t5 + Number")); dummy.ParseMembersAndMethods(parser); Assert.That( () => { - using var innerDummy = new Type(type.Package, + using var innerDummy = new Type(TestPackage.Instance, new TypeLines(nameof(CannotReassignValuesToImmutableMember), "has input = BaseClever(3)", "Run", "\tinput.Compute", "\tinput = BaseClever(5)")); innerDummy.ParseMembersAndMethods(parser).Methods[0].GetBodyAndParseIfNeeded(); @@ -246,10 +248,10 @@ public void CannotReassignValuesToImmutableMember() [Test] public void ModifyMutableMemberValueUsingTypeInstance() { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines("Clever", "mutable Number", "Compute Number", "\t5 + Number")); dummy.ParseMembersAndMethods(parser); - var cleverConsumerType = new Type(type.Package, + var cleverConsumerType = new Type(TestPackage.Instance, new TypeLines(nameof(ModifyMutableMemberValueUsingTypeInstance), "has clever = Clever(3)", "Run", "\tclever.Compute is 8", "\tclever.Number = 5")); cleverConsumerType.ParseMembersAndMethods(parser).Methods[0].GetBodyAndParseIfNeeded(); @@ -259,10 +261,10 @@ public void ModifyMutableMemberValueUsingTypeInstance() [Test] public void ModifyMutableMembersMultipleTimes() { - using var computer = new Type(type.Package, + using var computer = new Type(TestPackage.Instance, new TypeLines("Computer", "mutable Number", "Compute Number", "\t5 + Number")); computer.ParseMembersAndMethods(parser); - using var cleverConsumerType = new Type(type.Package, + using var cleverConsumerType = new Type(TestPackage.Instance, new TypeLines(nameof(ModifyMutableMembersMultipleTimes), "has computer = Computer(3)", "Run", "\tconstant five = 5", "\tmutable blub = Compute", "\tconstant number = five + 1", "\tmutable swappedBlub = blub", "\tblub = 49", "\tmutable temporary = swappedBlub", @@ -279,10 +281,10 @@ public void ModifyMutableMembersMultipleTimes() [Test] public void NewExpressionDoesNotMatchMemberType() { - using var dummyType = new Type(type.Package, + using var dummyType = new Type(TestPackage.Instance, new TypeLines("Dummy", "mutable Number", "Run", "\tNumber = 3", "\tNumber = 5")); dummyType.ParseMembersAndMethods(parser); - using var badType = new Type(type.Package, + using var badType = new Type(TestPackage.Instance, new TypeLines(nameof(NewExpressionDoesNotMatchMemberType), "mutable Number", "Compute Number", "\tNumber = \"Hi\"")); badType.ParseMembersAndMethods(parser); @@ -298,11 +300,11 @@ public void NewExpressionDoesNotMatchMemberType() public void CannotReassignNonMutableMember() { using var dummyType = - new Type(type.Package, + new Type(TestPackage.Instance, new TypeLines("DummyAgain", "mutable Number", "Run", "\tNumber = 3", "\tNumber = 5")); dummyType.ParseMembersAndMethods(parser); - using var badType = new Type(type.Package, + using var badType = new Type(TestPackage.Instance, new TypeLines(nameof(CannotReassignNonMutableMember), "constant something = 7", "Compute Number", "\tsomething = 3")); badType.ParseMembersAndMethods(parser); @@ -317,7 +319,7 @@ public void NotAllowedToReassignMethodCall() => Assert.That( () => { - using var dummy = new Type(type.Package, + using var dummy = new Type(TestPackage.Instance, new TypeLines(nameof(NotAllowedToReassignMethodCall), "mutable Number", "MutableCall Mutable(Number)", "\tMutableCall = Number", "\tNumber = 5")); dummy.ParseMembersAndMethods(parser).Methods[0].GetBodyAndParseIfNeeded(); diff --git a/Strict.Expressions.Tests/NumberTests.cs b/Strict.Expressions.Tests/NumberTests.cs index 48283d8a..b670edba 100644 --- a/Strict.Expressions.Tests/NumberTests.cs +++ b/Strict.Expressions.Tests/NumberTests.cs @@ -56,7 +56,7 @@ public void ParseTextToNumberUsingFromIsNotAllowed() => public void ParseNumberToText() { var methodCall = (MethodCall)ParseExpression("5 to Text"); - Assert.That(methodCall.ReturnType.Name, Is.EqualTo(Base.Text)); + Assert.That(methodCall.ReturnType.Name, Is.EqualTo(Type.Text)); Assert.That(methodCall.Instance, Is.EqualTo(new Number(method, 5))); } diff --git a/Strict.Expressions.Tests/Strict.Expressions.Tests.csproj b/Strict.Expressions.Tests/Strict.Expressions.Tests.csproj index ff12626f..aa7aeb0a 100644 --- a/Strict.Expressions.Tests/Strict.Expressions.Tests.csproj +++ b/Strict.Expressions.Tests/Strict.Expressions.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/Strict.Expressions.Tests/Strict.Expressions.Tests.v3.ncrunchproject b/Strict.Expressions.Tests/Strict.Expressions.Tests.v3.ncrunchproject deleted file mode 100644 index 5f26c2d5..00000000 --- a/Strict.Expressions.Tests/Strict.Expressions.Tests.v3.ncrunchproject +++ /dev/null @@ -1,9 +0,0 @@ - - - - - Strict.Expressions.Tests.NoConsoleWriteLineAllowed.ManualTestsCanWriteToConsole - - - - \ No newline at end of file diff --git a/Strict.Expressions.Tests/TestExpressions.cs b/Strict.Expressions.Tests/TestExpressions.cs index e06346c8..7bdb2e00 100644 --- a/Strict.Expressions.Tests/TestExpressions.cs +++ b/Strict.Expressions.Tests/TestExpressions.cs @@ -8,7 +8,7 @@ protected TestExpressions() { type = new Type(TestPackage.Instance, new TypeLines("dummy", "Run")). ParseMembersAndMethods(this); - type.GetType(Base.Boolean); + type.GetType(Type.Boolean); member = new Member(type, "logger", null); type.Members.Add(member); method = new Method(type, 0, this, [MethodTests.Run]); @@ -18,7 +18,7 @@ protected TestExpressions() type.Methods.AddRange(new List { method, methodWithBody }); numberFive = new Number(type, 5); list = new List(new Body(method), [numberFive]); - five = new Member(type, "five", type.GetType(Base.Number)) { InitialValue = numberFive }; + five = new Member(type, "five", type.GetType(Type.Number)) { InitialValue = numberFive }; type.Members.Add(five); } @@ -42,7 +42,7 @@ public void SayNoToConsoles() [TearDown] public void NoConsoleAllowed() { - TestPackage.Instance.Remove(type); + type.Dispose(); noConsole.CheckIfConsoleIsEmpty(); } @@ -78,11 +78,11 @@ protected Expression GetCondition(bool isNot = false) { var isExpression = CreateBinary(new MemberCall(null, five), BinaryOperator.Is, numberFive); return isNot - ? new Not(TestPackage.Instance.GetType(Base.Boolean).GetMethod(UnaryOperator.Not, []), + ? new Not(TestPackage.Instance.GetType(Type.Boolean).GetMethod(UnaryOperator.Not, []), isExpression) : isExpression; } protected Not CreateNot(Expression right) => - new(TestPackage.Instance.GetType(Base.Boolean).GetMethod(UnaryOperator.Not, []), right); + new(TestPackage.Instance.GetType(Type.Boolean).GetMethod(UnaryOperator.Not, []), right); } \ No newline at end of file diff --git a/Strict.Expressions.Tests/TextTests.cs b/Strict.Expressions.Tests/TextTests.cs index 2bae4a70..8cca6c04 100644 --- a/Strict.Expressions.Tests/TextTests.cs +++ b/Strict.Expressions.Tests/TextTests.cs @@ -11,7 +11,7 @@ public sealed class TextTests : TestExpressions public void ParseTextToNumber() { var methodCall = (MethodCall)ParseExpression("\"5\" to Number"); - Assert.That(methodCall.ReturnType.Name, Is.EqualTo(Base.Number)); + Assert.That(methodCall.ReturnType.Name, Is.EqualTo(Type.Number)); Assert.That(methodCall.Method.Name, Is.EqualTo("to")); Assert.That(methodCall.Instance?.ToString(), Is.EqualTo("\"5\"")); } @@ -38,15 +38,15 @@ public void TextExceededMaximumCharacterLimitUseMultiLine() => "\t\"SecondLineToMakeItThanHundredCharacters\" +", "\t\"ThirdLineToMakeItThanHundredCharacters\" +", "\t\"FourthLine\"", "\tresult is Text")] - public void - ParseMultiLineTextExpressions(string testName, string expectedOutput, - params string[] code) => - Assert.That( - ((Body)new Type(TestPackage.Instance, - new TypeLines(nameof(ParseMultiLineTextExpressions) + testName, code)). - ParseMembersAndMethods(new MethodExpressionParser()).Methods[0]. - GetBodyAndParseIfNeeded()).Expressions[0].ToString(), + public void ParseMultiLineTextExpressions(string testName, string expectedOutput, + params string[] code) + { + using var testType = new Type(TestPackage.Instance, + new TypeLines(nameof(ParseMultiLineTextExpressions) + testName, code)). + ParseMembersAndMethods(new MethodExpressionParser()); + Assert.That(((Body)testType.Methods[0].GetBodyAndParseIfNeeded()).Expressions[0].ToString(), Is.EqualTo(expectedOutput)); + } [TestCase("ParseNewLineTextExpression", "\"FirstLine\" + Character.NewLine + \"ThirdLine\" + Character.NewLine", "has logger", "Run Text", " \"FirstLine\" + Character.NewLine + \"ThirdLine\" + Character.NewLine")] @@ -56,8 +56,7 @@ public void public void ParseNewLineTextExpression(string testName, string expected, params string[] code) { using var multiLineType = new Type(TestPackage.Instance, - new TypeLines(testName, code)). - ParseMembersAndMethods(new MethodExpressionParser()); + new TypeLines(testName, code)).ParseMembersAndMethods(new MethodExpressionParser()); var binary = (Binary)multiLineType.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(binary.ToString(), Is.EqualTo(expected)); } diff --git a/Strict.Expressions.Tests/ValueInstanceTests.cs b/Strict.Expressions.Tests/ValueInstanceTests.cs new file mode 100644 index 00000000..b891a1c1 --- /dev/null +++ b/Strict.Expressions.Tests/ValueInstanceTests.cs @@ -0,0 +1,303 @@ +using Strict.Language.Tests; + +namespace Strict.Expressions.Tests; + +public sealed class ValueInstanceTests +{ + [SetUp] + public void CreateNumber() => numberType = TestPackage.Instance.GetType(Type.Number); + + private Type numberType = null!; + + [Test] + public void ToStringShowsTypeAndValue() => + Assert.That(new ValueInstance(numberType, 42d).ToString(), Is.EqualTo("Number: 42")); + + [Test] + public void CompareTwoNumbers() => + Assert.That(new ValueInstance(numberType, 42d), + Is.EqualTo(new ValueInstance(numberType, 42d))); + + [Test] + public void CompareNumberToText() => + Assert.That(new ValueInstance(numberType, 5d), + Is.Not.EqualTo(new ValueInstance("5"))); + + [Test] + public void CompareLists() + { + var listType = TestPackage.Instance.GetListImplementationType(numberType); + var nums1 = new List + { + new(numberType, 1d), + new(numberType, 2d), + new(numberType, 3d) + }; + var nums2 = new List + { + new(numberType, 1d), + new(numberType, 2d), + new(numberType, 3d) + }; + var nums3 = new List + { + new(numberType, 1d), + new(numberType, 2d), + new(numberType, 1d) + }; + var list = new ValueInstance(listType, nums1); + Assert.That(list, Is.EqualTo(new ValueInstance(listType, nums2))); + Assert.That(list, Is.Not.EqualTo(new ValueInstance(listType, nums3))); + } + + [Test] + public void ListWithValueInstancesWorks() + { + var listType = TestPackage.Instance.GetListImplementationType(numberType); + var items = new List { new Number(numberType, 1d).Data }; + Assert.That(new ValueInstance(listType, items), Is.Not.Null); + } + + [Test] + public void GenericListTypeAcceptsValueInstances() + { + var listType = TestPackage.Instance.GetType("List(key Generic, mappedValue Generic)"); + Assert.That(new ValueInstance(listType, new List()), Is.Not.Null); + } + + [Test] + public void CompareDictionaries() + { + var dictType = TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType); + var k1 = new ValueInstance(numberType, 1d); + var k2 = new ValueInstance(numberType, 2d); + var v2 = new ValueInstance(numberType, 2d); + var v3 = new ValueInstance(numberType, 3d); + var d1 = new Dictionary { { k1, v2 } }; + var d2 = new Dictionary { { new ValueInstance(numberType, 1d), new ValueInstance(numberType, 2d) } }; + var d3 = new Dictionary { { k2, v2 } }; + var d4 = new Dictionary { { k1, v3 } }; + var d5 = new Dictionary + { + { k1, v3 }, + { k2, v2 } + }; + var list = new ValueInstance(dictType, d1); + Assert.That(list, Is.EqualTo(new ValueInstance(dictType, d2))); + Assert.That(list, Is.Not.EqualTo(new ValueInstance(dictType, d3))); + Assert.That(list, Is.Not.EqualTo(new ValueInstance(dictType, d4))); + Assert.That(list, Is.Not.EqualTo(new ValueInstance(dictType, d5))); + } + + [Test] + public void CompareTypeContainingNumber() + { + using var t = + new Type(TestPackage.Instance, + new TypeLines(nameof(CompareTypeContainingNumber), "has number", "Run Boolean", + "\tnumber is 42")).ParseMembersAndMethods(new MethodExpressionParser()); + Assert.That(new ValueInstance(t, 42d), Is.EqualTo(new ValueInstance(numberType, 42d))); + } + + [Test] + public void ValueListInstanceStoresTypeAndItems() + { + var listType = TestPackage.Instance.GetListImplementationType(numberType); + var items = new List { new(numberType, 1d), new(numberType, 2d) }; + var instance = new ValueInstance(listType, items); + Assert.That(instance.ToString(), Does.Contain("List")); + } + + [Test] + public void ValueDictionaryInstanceStoresTypeAndItems() + { + var dictType = TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType); + var dict = new Dictionary + { + { new ValueInstance(numberType, 1), new ValueInstance(numberType, 2) } + }; + var instance = new ValueInstance(dictType, dict); + Assert.That(instance.ToString(), Does.Contain("Dictionary")); + } + + [Test] + public void ValueTypeInstanceStoresMembers() + { + using var t = new Type(TestPackage.Instance, + new TypeLines(nameof(ValueTypeInstanceStoresMembers), "has number", "Run Boolean", + "\tnumber is 1")).ParseMembersAndMethods(new MethodExpressionParser()); + var instance = new ValueInstance(t, + new Dictionary { { "number", new ValueInstance(numberType, 7) } }); + Assert.That(instance.ToString(), Does.Contain(nameof(ValueTypeInstanceStoresMembers))); + } + + [Test] + public void ThrowsInvalidTypeValueWhenUsingReservedNumberForText() => + Assert.Throws(() => + _ = new ValueInstance(numberType, -7.90897526e307)); + + [Test] + public void ThrowsWhenCreatingTypeInstanceForNumberType() => + Assert.Throws(() => + _ = new ValueInstance(numberType, new Dictionary())); + + [Test] + public void CopyConstructorWithDictionaryCreatesNewReturnType() + { + var dictType = TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType); + var mutableDictType = + TestPackage.Instance.GetType(Type.Mutable).GetGenericImplementation(dictType); + var original = new ValueInstance(dictType, + new Dictionary + { + { new ValueInstance(numberType, 1d), new ValueInstance(numberType, 2d) } + }); + var copy = new ValueInstance(original, mutableDictType); + Assert.That(copy.IsDictionary, Is.True); + Assert.That(copy.IsMutable, Is.True); + } + + [Test] + public void CopyConstructorWithMutableTextTypeIdConvertsBackToTextId() + { + var textType = TestPackage.Instance.GetType(Type.Text); + var mutableTextType = + TestPackage.Instance.GetType(Type.Mutable).GetGenericImplementation(textType); + var textInstance = new ValueInstance("hello"); + var mutableTextInstance = new ValueInstance(textInstance, mutableTextType); + var converted = new ValueInstance(mutableTextInstance, textType); + Assert.That(converted.IsText, Is.True); + Assert.That(converted.Text, Is.EqualTo("hello")); + } + + [Test] + public void CopyConstructorWithTypeIdCreatesNewReturnType() + { + using var originalType = new Type(TestPackage.Instance, + new TypeLines(nameof(CopyConstructorWithTypeIdCreatesNewReturnType) + "A", "has number", + "Run Boolean", "\tnumber is 1")).ParseMembersAndMethods(new MethodExpressionParser()); + using var newType = new Type(TestPackage.Instance, + new TypeLines(nameof(CopyConstructorWithTypeIdCreatesNewReturnType) + "B", "has number", + "Run Boolean", "\tnumber is 1")).ParseMembersAndMethods(new MethodExpressionParser()); + var members = new Dictionary + { + { "number", new ValueInstance(numberType, 5d) } + }; + var original = new ValueInstance(originalType, members); + var copy = new ValueInstance(original, newType); + Assert.That(copy.TryGetValueTypeInstance()!.ReturnType, Is.EqualTo(newType)); + } + + [Test] + public void IsTypeReturnsTrueForDictionaryInstanceMatchingType() + { + var dictType = TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType); + var instance = new ValueInstance(dictType, new Dictionary()); + Assert.That(instance.IsType(dictType), Is.True); + Assert.That(instance.IsType(numberType), Is.False); + } + + [Test] + public void ApplyMethodReturnTypeMutableConvertsFromMutableToImmutable() + { + var listType = TestPackage.Instance.GetListImplementationType(numberType); + var mutableListType = + TestPackage.Instance.GetType(Type.Mutable).GetGenericImplementation(listType); + var mutableInstance = + new ValueInstance(mutableListType, new List { new(numberType, 1d) }); + var result = mutableInstance.ApplyMethodReturnTypeMutable(listType); + Assert.That(result.IsMutable, Is.False); + Assert.That(result.IsList, Is.True); + } + + [Test] + public void GetTypeExceptTextReturnsListReturnTypeForListInstance() + { + var listType = TestPackage.Instance.GetListImplementationType(numberType); + var instance = new ValueInstance(listType, new List()); + Assert.That(instance.GetTypeExceptText(), Is.EqualTo(listType)); + } + + [Test] + public void GetIteratorLengthThrowsForDictionaryInstance() + { + var dictType = TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType); + var instance = new ValueInstance(dictType, new Dictionary()); + Assert.Throws(() => _ = instance.GetIteratorLength()); + } + + [Test] + public void GetIteratorLengthForTypeIdWithKeysAndValuesMember() + { + using var customType = new Type(TestPackage.Instance, + new TypeLines(nameof(GetIteratorLengthForTypeIdWithKeysAndValuesMember), "has number", + "Run Number", "\t5")).ParseMembersAndMethods(new MethodExpressionParser()); + var listType = TestPackage.Instance.GetListImplementationType(numberType); + var listInstance = new ValueInstance(listType, + new List { new(numberType, 1d), new(numberType, 2d), new(numberType, 3d) }); + var members = new Dictionary + { + { "number", new ValueInstance(numberType, 1d) }, + { "keysAndValues", listInstance } + }; + var instance = new ValueInstance(customType, members); + Assert.That(instance.GetIteratorLength(), Is.EqualTo(3)); + } + + [Test] + public void GetIteratorValueForTypeIdWithElementsMember() + { + using var customType = new Type(TestPackage.Instance, + new TypeLines(nameof(GetIteratorValueForTypeIdWithElementsMember), "has number", + "Run Number", "\t5")).ParseMembersAndMethods(new MethodExpressionParser()); + var listType = TestPackage.Instance.GetListImplementationType(numberType); + var item1 = new ValueInstance(numberType, 10d); + var item2 = new ValueInstance(numberType, 20d); + var listInstance = new ValueInstance(listType, new List { item1, item2 }); + var members = new Dictionary + { + { "number", new ValueInstance(numberType, 1d) }, + { "elements", listInstance } + }; + var instance = new ValueInstance(customType, members); + var charType = TestPackage.Instance.GetType(Type.Character); + Assert.That(instance.GetIteratorValue(charType, 1), Is.EqualTo(item2)); + } + + [Test] + public void GetIteratorValueThrowsForUnsupportedInstance() + { + using var customType = new Type(TestPackage.Instance, + new TypeLines(nameof(GetIteratorValueThrowsForUnsupportedInstance), "has number", + "Run Number", "\t5")).ParseMembersAndMethods(new MethodExpressionParser()); + var instance = new ValueInstance(customType, + new Dictionary { { "number", new ValueInstance(numberType, 1d) } }); + var charType = TestPackage.Instance.GetType(Type.Character); + Assert.Throws(() => + _ = instance.GetIteratorValue(charType, 0)); + } + + [Test] + public void EqualsReturnsTrueWhenTypeIdHasNumberMemberMatchingPrimitive() + { + using var t = new Type(TestPackage.Instance, + new TypeLines("TypeIdNumberMemberMatchesPrimitive", + "has number", "Run Boolean", + "\tnumber is 1")).ParseMembersAndMethods(new MethodExpressionParser()); + var typeInstance = new ValueInstance(t, + new Dictionary { { "number", new ValueInstance(numberType, 42d) } }); + Assert.That(typeInstance, Is.EqualTo(new ValueInstance(numberType, 42d))); + } + + [Test] + public void EqualsReturnsTrueWhenPrimitiveMatchesTypeIdWithNumberMember() + { + using var t = new Type(TestPackage.Instance, + new TypeLines("PrimitiveMatchesTypeIdNumberMember", + "has number", "Run Boolean", + "\tnumber is 1")).ParseMembersAndMethods(new MethodExpressionParser()); + var typeInstance = new ValueInstance(t, + new Dictionary { { "number", new ValueInstance(numberType, 42d) } }); + Assert.That(new ValueInstance(numberType, 42d), Is.EqualTo(typeInstance)); + } +} \ No newline at end of file diff --git a/Strict.Expressions/Binary.cs b/Strict.Expressions/Binary.cs index 0b6e0e7d..0fb71273 100644 --- a/Strict.Expressions/Binary.cs +++ b/Strict.Expressions/Binary.cs @@ -6,13 +6,23 @@ namespace Strict.Expressions; public sealed class Binary(Expression left, Method operatorMethod, Expression[] right) : MethodCall(operatorMethod, left, right, null, left.LineNumber) { + /// + /// For "in" we have to swap left and right (in is always implemented in the Iterator) + /// public override string ToString() => - // For "in" we have to swap left and right (in is always implemented in the Iterator) Method.Name is BinaryOperator.In - ? AddNestedBracketsIfNeeded(Arguments[0]) + " is in " + AddNestedBracketsIfNeeded(Instance!) - : AddNestedBracketsIfNeeded(Instance!) + " " + (Method.Name is UnaryOperator.Not - ? "is " - : "") + Method.Name + " " + AddNestedBracketsIfNeeded(Arguments[0]); + ? $"{AddNestedBracketsIfNeeded(Arguments[0])} is in {AddNestedBracketsIfNeeded(Instance!)}" + : $"{ + AddNestedBracketsIfNeeded(Instance!) + } { + (Method.Name is UnaryOperator.Not + ? "is " + : "") + }{ + Method.Name + } { + AddNestedBracketsIfNeeded(Arguments[0]) + }"; private string AddNestedBracketsIfNeeded(Expression child) => child is MethodCall binaryOrUnary && BinaryOperator.GetPrecedence(binaryOrUnary.Method.Name) < @@ -20,8 +30,7 @@ private string AddNestedBracketsIfNeeded(Expression child) => ? $"({child})" : child.ToString(); - public static Expression - Parse(Body body, ReadOnlySpan input, Stack postfixTokens) + public static Expression Parse(Body body, ReadOnlySpan input, Stack postfixTokens) { #if LOG_OPERATORS_PARSING Console.WriteLine(); @@ -34,7 +43,7 @@ public static Expression public sealed class IncompleteTokensForBinaryExpression(Body body, ReadOnlySpan input, IEnumerable postfixTokens) : ParsingFailed(body, //ncrunch: no coverage - input.GetTextsFromRanges(postfixTokens).Reverse().ToWordList()); + string.Join(", ", input.GetTextsFromRanges(postfixTokens).Reverse())); private static Expression BuildBinaryExpression(Body body, ReadOnlySpan input, Range operatorTokenRange, Stack tokens) diff --git a/Strict.Expressions/Boolean.cs b/Strict.Expressions/Boolean.cs index fe6cf768..f62f7007 100644 --- a/Strict.Expressions/Boolean.cs +++ b/Strict.Expressions/Boolean.cs @@ -1,15 +1,19 @@ using System.Runtime.CompilerServices; using Strict.Language; +using Type = Strict.Language.Type; namespace Strict.Expressions; /// -/// Constant boolean that appears anywhere in the parsed code, simply "true" or "false" +/// Constant boolean that appears anywhere in the parsed code, "true" or "false" /// public sealed class Boolean(Context context, bool value, int lineNumber = 0) - : Value(context.GetType(Base.Boolean), value, lineNumber) + : Value(context.GetType(Type.Boolean), value, lineNumber) { - public override string ToString() => base.ToString().ToLower(); + public override string ToString() => + Data.number == 0 + ? "false" + : "true"; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Expression? TryParse(Body body, ReadOnlySpan line) => diff --git a/Strict.Expressions/Declaration.cs b/Strict.Expressions/Declaration.cs index bf6ea4d3..46cb32d7 100644 --- a/Strict.Expressions/Declaration.cs +++ b/Strict.Expressions/Declaration.cs @@ -1,4 +1,5 @@ using Strict.Language; +using Type = Strict.Language.Type; namespace Strict.Expressions; @@ -51,7 +52,7 @@ public override string ToString() => /// /// Highly optimized parsing of assignments, skips over the mutable, grabs the name of the local - /// variable, then skips over the space, equal and space characters and parses the rest, e.g. + /// variable, then skips over the space, equal, and space characters, and parses the rest, e.g., /// constant hello = "hello" + " " + "world" /// ^ ^ ^ ^ ^ ^ END, using TryParseExpression with Range(12, 35) /// @@ -81,20 +82,21 @@ private static Expression CreateMutableDeclaration(Body body, ReadOnlySpan string name) { var value = valueSpan.IsFirstLetterUppercase() && (valueSpan.IsPlural() || - valueSpan.StartsWith(Base.List + '(' + Base.Mutable, StringComparison.Ordinal)) + valueSpan.StartsWith(Type.List + '(' + Type.Mutable, StringComparison.Ordinal)) ? new List(body.Method.Type.GetType(valueSpan.ToString())) : body.Method.ParseExpression(body, valueSpan, true); return new Declaration(body, name, value, true); } - private static bool IsExpressionFullyConstant(Expression expr) + private static bool IsExpressionFullyConstant(Expression? expr) { - if (!expr.IsConstant) + if (expr is not { IsConstant: true }) return false; return expr switch { MethodCall methodCall => methodCall.Arguments.All(IsExpressionFullyConstant), - MemberCall memberCall => IsExpressionFullyConstant(memberCall.Member.InitialValue!), + MemberCall memberCall => IsExpressionFullyConstant(memberCall.Member.InitialValue), + VariableCall variableCall => IsExpressionFullyConstant(variableCall.Variable.InitialValue), List list => list.Values.All(IsExpressionFullyConstant), _ => true }; diff --git a/Strict.Expressions/Dictionary.cs b/Strict.Expressions/Dictionary.cs index 84f3e6d6..83a0cf6f 100644 --- a/Strict.Expressions/Dictionary.cs +++ b/Strict.Expressions/Dictionary.cs @@ -6,7 +6,8 @@ namespace Strict.Expressions; public sealed class Dictionary : Value { public Dictionary(IReadOnlyList types, Type dictionaryImplementationType) : base( - dictionaryImplementationType, CreateEmptyMembers(dictionaryImplementationType)) + dictionaryImplementationType, new ValueInstance(dictionaryImplementationType, + new Dictionary())) { if (types.Count != 2) throw new DictionaryMustBeInitializedWithTwoTypeParameters(dictionaryImplementationType, @@ -17,28 +18,16 @@ public Dictionary(IReadOnlyList types, Type dictionaryImplementationType) public Type KeyType { get; } public Type MappedValueType { get; } - - private static object CreateEmptyMembers(Type dictionaryImplementationType) - { - var listMemberName = dictionaryImplementationType.Members.FirstOrDefault(member => - member.Type is GenericTypeImplementation { Generic.Name: Base.List } || - member.Type.Name == Base.List)?.Name ?? Type.ElementsLowercase; - return new System.Collections.Generic.Dictionary(StringComparer.Ordinal) - { - [listMemberName] = new System.Collections.Generic.List() - }; - } - public override string ToString() => ReturnType.Name; public static Expression? TryParse(Body body, ReadOnlySpan input) => - input.StartsWith(Base.Dictionary + '(') && input[^1] == ')' && AreTypeParameters(body, input) + input.StartsWith(Type.Dictionary + '(') && input[^1] == ')' && AreTypeParameters(body, input) ? new Dictionary(ParseTypeParameters(body, input), body.Method.GetType(input.ToString())) : null; private static bool AreTypeParameters(Body body, ReadOnlySpan input) { - foreach (var typeText in input[(Base.Dictionary.Length + 1)..^1]. + foreach (var typeText in input[(Type.Dictionary.Length + 1)..^1]. Split(',', StringSplitOptions.TrimEntries)) if (body.Method.FindType(typeText.ToString()) == null) return false; @@ -48,7 +37,7 @@ private static bool AreTypeParameters(Body body, ReadOnlySpan input) private static List ParseTypeParameters(Body body, ReadOnlySpan input) { var types = new List(); - foreach (var typeText in input[(Base.Dictionary.Length + 1)..^1]. + foreach (var typeText in input[(Type.Dictionary.Length + 1)..^1]. Split(',', StringSplitOptions.TrimEntries)) types.Add(body.Method.GetType(typeText.ToString())); return types.Count != 2 @@ -63,7 +52,7 @@ public DictionaryMustBeInitializedWithTwoTypeParameters(Type type, IReadOnlyColl $"Expected Type Parameters: 2, Given type parameters: { types.Count } and they are { - types.ToWordList() + string.Join(", ", types) }") { } public DictionaryMustBeInitializedWithTwoTypeParameters(Body body, string expressionText) : diff --git a/Strict.Expressions/For.cs b/Strict.Expressions/For.cs index fbd95b12..186f30fb 100644 --- a/Strict.Expressions/For.cs +++ b/Strict.Expressions/For.cs @@ -22,7 +22,7 @@ public override string ToString() => private string InCustomVariables() => CustomVariables.Length > 0 - ? CustomVariables.ToWordList() + " in " + ? string.Join(", ", CustomVariables) + " in " : ""; public override bool IsConstant => Iterator.IsConstant && Body.IsConstant; @@ -89,13 +89,13 @@ private static Expression ParseFor(Body body, ReadOnlySpan line, Body inne CheckForIncorrectMatchingTypes(innerBody, variableNames, iterator); else AddImplicitVariables(body, line, innerBody); - if (!GetIteratorType(iterator).IsIterator && iterator.ReturnType.Name != Base.Number) + if (!GetIteratorType(iterator).IsIterator && !iterator.ReturnType.IsNumber) throw new ExpressionTypeIsNotAnIterator(body, iterator.ReturnType.Name, line[4..].ToString()); var forExpression = new For(variables, iterator, innerBody.Parse(), body.CurrentFileLineNumber); #if DEBUG var originalLines = line.ToString() + Environment.NewLine + - body.Method.GetLinesAndStripTabs(innerBody.LineRange, body).ToWordList(Environment.NewLine); + body.Method.GetLinesAndStripTabs(innerBody.LineRange, body).ToLines(); var generatedLines = forExpression.ToString(); if (generatedLines != originalLines && !body.Method.GetLinesAndStripTabs(innerBody.LineRange, body).Any(l => @@ -109,11 +109,11 @@ private static bool HasIn(ReadOnlySpan line) => line.Contains(InWithSpaces, StringComparison.Ordinal); private const string InWithSpaces = " in "; - +#if DEBUG private sealed class GeneratedForExpressionDoesNotMatchInputExactly(Body body, Expression @for, string line) : ParsingFailed(body, "\n" + //ncrunch: no coverage @for.ToString().Replace("\t", " ") + "\nOriginal lines:\n" + line.Replace("\t", " ")); - +#endif private static Expression ParseWithImplicitVariable(Body body, ReadOnlySpan line, Body innerBody) { @@ -127,13 +127,12 @@ private static void AddImplicitVariables(Body body, ReadOnlySpan line, Bod if (innerBody.FindVariable(Type.IndexLowercase) != null && innerBody.FindVariable(Type.ValueLowercase) != null) return; - innerBody.AddVariable(Type.IndexLowercase, new Number(body.Method, 0), false); + innerBody.AddVariable(Type.IndexLowercase, new Number(body.Method, 0), true); var valueExpression = body.Method.ParseExpression(body, GetVariableExpressionValue(body, line), true); - if (valueExpression.ReturnType is GenericTypeImplementation { Generic.Name: Base.List } || - valueExpression.ReturnType.Name == Base.List) + if (valueExpression.ReturnType.IsList) valueExpression = new ListCall(valueExpression, new Number(body.Method, 0)); - innerBody.AddVariable(Type.ValueLowercase, valueExpression, false); + innerBody.AddVariable(Type.ValueLowercase, valueExpression, true); } private static string GetVariableExpressionValue(Body body, ReadOnlySpan line, @@ -200,9 +199,9 @@ private static Expression GetVariableValue(Body body, ReadOnlySpan line, i { var forIteratorText = GetForIteratorText(line); var iterator = body.Method.ParseExpression(body, forIteratorText, true); - if (iterator is MethodCall { ReturnType.Name: Base.Range } methodCall) + if (iterator is MethodCall { ReturnType.Name: Type.Range } methodCall) return GetVariableValueFromRange(iterator, methodCall); - if (iterator.ReturnType is not GenericTypeImplementation { Generic.Name: Base.List }) + if (iterator.ReturnType is not GenericTypeImplementation { Generic.Name: Type.List }) return iterator; var firstValue = body.Method.ParseExpression(body, forIteratorText[^1] == ')' ? forIteratorText[1..forIteratorText.IndexOf(',')] @@ -220,7 +219,7 @@ private static Expression GetVariableValueFromRange(Expression iterator, MethodC ? methodCall.Arguments[0] : methodCall.Instance is MethodCall { - ReturnType.Name: Base.Range, Arguments.Count: > 0 + ReturnType.Name: Type.Range, Arguments.Count: > 0 } innerMethodCall ? innerMethodCall.Arguments[0] : iterator; @@ -251,7 +250,7 @@ private static void CheckForIncorrectMatchingTypes(Body innerBody, ReadOnlySpan< for (var depth = 0; depth < implementationDepth; depth++) if (iteratorType is GenericTypeImplementation { IsIterator: true } genericType) iteratorType = genericType.ImplementationTypes[0]; - if ((iteratorType.Name != Base.Range || mutableValue.Type.Name != Base.Number) && + if ((iteratorType.Name != Type.Range || !mutableValue.Type.IsNumber) && iteratorType.Name != mutableValue.Type.Name && !iteratorType.IsSameOrCanBeUsedAs(mutableValue.Type, false)) throw new IteratorTypeDoesNotMatchWithIterable(innerBody, iteratorType.Name, variable, diff --git a/Strict.Expressions/If.cs b/Strict.Expressions/If.cs index 3a87bce3..69daf940 100644 --- a/Strict.Expressions/If.cs +++ b/Strict.Expressions/If.cs @@ -163,7 +163,7 @@ private static Expression CreateSelectorCondition(Expression selector, Expressio private static Expression GetConditionExpression(Body body, ReadOnlySpan line) { var condition = body.Method.ParseExpression(body, line); - var booleanType = condition.ReturnType.GetType(Base.Boolean); + var booleanType = condition.ReturnType.GetType(Type.Boolean); if (condition.ReturnType == booleanType || booleanType.IsSameOrCanBeUsedAs(condition.ReturnType, false)) return condition; @@ -172,7 +172,8 @@ private static Expression GetConditionExpression(Body body, ReadOnlySpan l public sealed class InvalidCondition(Body body, Type? conditionReturnType = null) : ParsingFailed(body, conditionReturnType != null - ? body.Method.FullName + "\n Return type " + conditionReturnType + " is not " + Base.Boolean + ? body.Method.FullName + "\n Return type " + conditionReturnType + " is not " + + Type.Boolean : null); public sealed class MissingThen(Body body) : ParsingFailed(body); diff --git a/Strict.Expressions/Instance.cs b/Strict.Expressions/Instance.cs index 80d3e011..5d2d18f4 100644 --- a/Strict.Expressions/Instance.cs +++ b/Strict.Expressions/Instance.cs @@ -16,4 +16,9 @@ public static Expression Parse(Body body, Method method) public override bool IsConstant => false; public override string ToString() => Type.ValueLowercase; + //ncrunch: no coverage start + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is Instance i && ReturnType == i.ReturnType); + public override int GetHashCode() => ReturnType.GetHashCode(); } \ No newline at end of file diff --git a/Strict.Expressions/List.cs b/Strict.Expressions/List.cs index 2f621b70..1838893e 100644 --- a/Strict.Expressions/List.cs +++ b/Strict.Expressions/List.cs @@ -6,28 +6,87 @@ namespace Strict.Expressions; public sealed class List : Value { public List(Body bodyForErrorMessage, List values, bool isMutable = false) : base( - values[0].ReturnType.GetListImplementationType( - GetCommonBaseType(values.Select(v => v.ReturnType).ToList(), bodyForErrorMessage)), - values, values[0].LineNumber, isMutable) => - Values = values; - - public List(Type type, int lineNumber = 0) : base(type, Array.Empty(), lineNumber, - true) => - Values = []; - - private static Type - GetCommonBaseType(IReadOnlyList returnTypes, Body bodyForErrorMessage) => - returnTypes.Count == 1 || returnTypes.All(t => t == returnTypes[0]) || - returnTypes.Any(t => t.Members.Any(member => member.Type == returnTypes[0])) - ? returnTypes[0] - : returnTypes.FirstOrDefault(t => returnTypes[0].Members.Any(m => m.Type == t)) ?? - throw new ListElementsMustHaveMatchingType(bodyForErrorMessage, returnTypes); - - public sealed class ListElementsMustHaveMatchingType(Body body, IEnumerable returnTypes) - : ParsingFailed(body, "List has one or many mismatching types " + string.Join(", ", returnTypes)); + values[0].ReturnType.GetListImplementationType(GetCommonBaseType(values, bodyForErrorMessage)), + new List(), values[0].LineNumber, isMutable) => Values = values; + + public List(Type type, int lineNumber = 0) : base(type, new List(), lineNumber, + true) => Values = []; + + private static Type GetCommonBaseType(IReadOnlyList values, Body bodyForErrorMessage) + { + var firstType = values[0].ReturnType; + if (values.Count == 1) + return firstType; + var allSameType = true; + for (var i = 1; i < values.Count; i++) + if (values[i].ReturnType != firstType) + { + allSameType = false; + break; + } + if (allSameType) + return firstType; + for (var i = 0; i < values.Count; i++) + { + var members = values[i].ReturnType.Members; + for (var j = 0; j < members.Count; j++) + if (members[j].Type == firstType) + return firstType; + } + for (var i = 0; i < values.Count; i++) + { + var members = firstType.Members; + for (var j = 0; j < members.Count; j++) + if (members[j].Type == values[i].ReturnType) + return values[i].ReturnType; //ncrunch: no coverage + } + throw new ListElementsMustHaveMatchingType(bodyForErrorMessage, values); + } + + public sealed class ListElementsMustHaveMatchingType(Body body, IReadOnlyList values) + : ParsingFailed(body, "List has one or many mismatching types " + string.Join(", ", values)); public List Values { get; } - public override bool IsConstant => Values.All(v => v.IsConstant); + public override bool IsConstant + { + get + { + for (var i = 0; i < Values.Count; i++) + if (!Values[i].IsConstant) + return false; + return true; + } + } + + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is List list && ReturnType == list.ReturnType && + Values.Count == list.Values.Count && ValuesEqual(list)); + + private bool ValuesEqual(List other) + { + for (var i = 0; i < Values.Count; i++) + if (!Values[i].Equals(other.Values[i])) + return false; //ncrunch: no coverage + return true; + } + + public override int GetHashCode() => + Values.Count > 0 //ncrunch: no coverage + ? ReturnType.GetHashCode() ^ Values[0].GetHashCode() ^ Values.Count + : ReturnType.GetHashCode(); + + public ValueInstance? TryGetConstantData() + { + if (!IsConstant) + return null; + var valueInstances = new ValueInstance[Values.Count]; + for (var i = 0; i < Values.Count; i++) + valueInstances[i] = ((Value)Values[i]).Data; + return new ValueInstance(ReturnType, valueInstances); + } + + public new ValueInstance Data => throw new NotSupportedException("Use TryGetConstantData instead!"); public override string ToString() { diff --git a/Strict.Expressions/ListCall.cs b/Strict.Expressions/ListCall.cs index 16fa8221..bb34d212 100644 --- a/Strict.Expressions/ListCall.cs +++ b/Strict.Expressions/ListCall.cs @@ -51,7 +51,7 @@ private static ListCall CreateListCallAndCheckIndexBounds(Body body, Expression } memberCall) index = memberCall.Member.InitialValue; if (index is Number indexNumber) - return (int)(double)indexNumber.Data; + return (int)indexNumber.Data.number; return null; } @@ -64,7 +64,7 @@ private static ListCall CreateListCallAndCheckIndexBounds(Body body, Expression return memberCall.Instance as List; } - private static Expression? FindLengthConstraint(IEnumerable? constraints) + private static Expression? FindLengthConstraint(IReadOnlyList? constraints) { if (constraints != null) foreach (var constraint in constraints) @@ -77,7 +77,7 @@ private static ListCall CreateListCallAndCheckIndexBounds(Body body, Expression private static int? ExtractLengthValue(Expression lengthConstraint) => lengthConstraint is Binary { Arguments.Count: > 0 } binary && binary.Arguments[0] is Number lengthNumber - ? (int)(double)lengthNumber.Data + ? (int)lengthNumber.Data.number : null; public sealed class NegativeIndexIsNeverAllowed(Body body, Expression list) : @@ -93,4 +93,10 @@ public sealed class IndexViolatesListConstraint(Body body, int index, Expression public override bool IsConstant => List.IsConstant && Index.IsConstant; public override string ToString() => $"{List}({Index})"; + //ncrunch: no coverage start + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is ListCall lc && List.Equals(lc.List) && Index.Equals(lc.Index)); + + public override int GetHashCode() => List.GetHashCode() ^ Index.GetHashCode(); } \ No newline at end of file diff --git a/Strict.Expressions/MemberCall.cs b/Strict.Expressions/MemberCall.cs index 319c3773..f4ec7277 100644 --- a/Strict.Expressions/MemberCall.cs +++ b/Strict.Expressions/MemberCall.cs @@ -13,11 +13,12 @@ public sealed class MemberCall(Expression? instance, Member member, int lineNumb public static Expression? TryParse(Body body, Type type, Expression? instance, ReadOnlySpan partToParse) { - foreach (var member in type.Members) - if (partToParse.Equals(member.Name, StringComparison.Ordinal)) + var members = type.Members; + for (var i = 0; i < members.Count; i++) + if (partToParse.Equals(members[i].Name, StringComparison.Ordinal)) return instance == null && body.IsFakeBodyForMemberInitialization ? throw new CannotAccessMemberBeforeTypeIsParsed(body, partToParse.ToString(), type) - : new MemberCall(instance, member, body.CurrentFileLineNumber); + : new MemberCall(instance, members[i], body.CurrentFileLineNumber); return body.Method.Name == Member.ConstraintsBody ? FindContainingMethodTypeMemberForConstraints(body, instance, partToParse.ToString()) : null; @@ -38,4 +39,12 @@ public override string ToString() => Instance != null ? $"{Instance}.{Member.Name}" : Member.Name; + + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is MemberCall mc && Member.Name == mc.Member.Name && + Member.Type == mc.Member.Type && Equals(Instance, mc.Instance)); + + public override int GetHashCode() => + Member.GetHashCode() ^ (Instance?.GetHashCode() ?? 0); } \ No newline at end of file diff --git a/Strict.Expressions/MethodCall.cs b/Strict.Expressions/MethodCall.cs index 4dddae6a..ce51c152 100644 --- a/Strict.Expressions/MethodCall.cs +++ b/Strict.Expressions/MethodCall.cs @@ -5,7 +5,7 @@ namespace Strict.Expressions; /// /// Any type of method we can call, this includes normal local method calls, recursions, calls to -/// any of our implement base types (instance is null in all of those cases), calls to other types +/// any of our implement base types (instance is null in all of those cases). Calls to other types /// (either From(type) or instance method calls, there are no static methods) or any operator /// or unary call (which are all normal methods as well). /// Like MemberCall has the same syntax when the parent instance is used: Type.Method @@ -33,8 +33,18 @@ private static Type GetMethodReturnType(Method method, Type? toReturnType) => public Method Method { get; } public Expression? Instance { get; } public IReadOnlyList Arguments { get; } - public override bool IsConstant => - (Instance?.IsConstant ?? true) && Arguments.All(a => a.IsConstant); + public override bool IsConstant + { + get + { + if (Instance?.IsConstant == false) + return false; + for (var index = 0; index < Arguments.Count; index++) + if (!Arguments[index].IsConstant) + return false; + return true; + } + } // ReSharper disable once TooManyArguments public static Expression? TryParse(Expression? instance, Body body, @@ -70,7 +80,7 @@ private static bool AreArgumentsCompatible(Method method, IReadOnlyList arguments) { - if (fromType.Name == Base.List && fromType.IsGeneric && arguments.Count > 0) + if (fromType is { IsList: true, IsGeneric: true } && arguments.Count > 0) return fromType.GetGenericImplementation(arguments[0].ReturnType); - if (fromType.Name != Base.Dictionary || !fromType.IsGeneric) + if (!fromType.IsDictionary || !fromType.IsGeneric) return fromType; if (arguments.Count > 1) return arguments[0] is List { Values.Count: 2 } firstPair @@ -128,17 +138,17 @@ private static Type NormalizeListAndDictionaryImplementation(Type fromType, private static void ValidateMutableImplementation(Type fromType, IReadOnlyList args) { - if (fromType.Name == Base.Mutable && fromType.IsGeneric && args.Count == 1 && - args[0].ReturnType is not GenericTypeImplementation { Generic.Name: Base.List }) + if (fromType is { IsMutable: true, IsGeneric: true } && args is + [{ ReturnType: not GenericTypeImplementation { Generic.Name: Type.List } }]) throw new Type.GenericTypesCannotBeUsedDirectlyUseImplementation(fromType, - Base.Mutable + " must be used with a List implementation"); - if (fromType is GenericTypeImplementation { Generic.Name: Base.Mutable } mutableImpl && + Type.Mutable + " must be used with a List implementation"); + if (fromType is GenericTypeImplementation { Generic.Name: Type.Mutable } mutableImpl && mutableImpl.ImplementationTypes[0] is not GenericTypeImplementation { - Generic.Name: Base.List + Generic.Name: Type.List }) throw new Type.GenericTypesCannotBeUsedDirectlyUseImplementation(mutableImpl, - Base.Mutable + " must be used with a List implementation"); + Type.Mutable + " must be used with a List implementation"); } private static IReadOnlyList NormalizeDictionaryArguments(Body body, Type fromType, @@ -148,7 +158,7 @@ private static IReadOnlyList NormalizeDictionaryArguments(Body body, return arguments; if (arguments.Count > 1) return [new List(body, arguments.ToList())]; - return arguments.Count == 1 && arguments[0] is List { Values.Count: 2 } singlePair + return arguments is [List { Values.Count: 2 } singlePair] ? [new List(body, [singlePair])] : arguments; } @@ -156,47 +166,47 @@ private static IReadOnlyList NormalizeDictionaryArguments(Body body, private static IReadOnlyList NormalizeErrorArguments(Body body, ref Type fromType, IReadOnlyList arguments, Expression? basedOnErrorVariable) { - if (!fromType.IsSameOrCanBeUsedAs(fromType.GetType(Base.Error))) + if (!fromType.IsSameOrCanBeUsedAs(fromType.GetType(Type.Error))) return arguments; if (arguments.Count == 0) return [ - new Value(body.Method.GetType(Base.Name), basedOnErrorVariable?.ToString() ?? - (fromType.Name == Base.Error + new Value(body.Method.GetType(nameof(Type.Name)), + basedOnErrorVariable?.ToString() ?? (fromType.IsError ? body.CurrentDeclarationNameForErrorText ?? body.Method.Name : fromType.Name)), - CreateListFromMethodCall(body, Base.Stacktrace, CreateStacktraces(body)) + CreateListFromMethodCall(body, Type.Stacktrace, CreateStacktraces(body)) ]; if (arguments.Count > 1) throw new Type.ArgumentsDoNotMatchMethodParameters(arguments, fromType, fromType.Methods); //ncrunch: no coverage if (basedOnErrorVariable != null) { - fromType = fromType.GetType(Base.ErrorWithValue). + fromType = fromType.GetType(Type.ErrorWithValue). GetGenericImplementation(arguments[0].ReturnType); return [basedOnErrorVariable, arguments[0]]; } - if (arguments[0] is Value { ReturnType.Name: Base.Text } textValue) + if (arguments[0] is Value { ReturnType.Name: Type.Text } textValue) return [ - new Value(body.Method.GetType(Base.Name), textValue.Data.ToString() ?? ""), - CreateListFromMethodCall(body, Base.Stacktrace, CreateStacktraces(body)) + new Value(body.Method.GetType(nameof(Type.Name)), textValue.Data.ToString()), + CreateListFromMethodCall(body, Type.Stacktrace, CreateStacktraces(body)) ]; arguments = [ CreateFromMethodCall(body, fromType, []), arguments[0] ]; - fromType = fromType.GetType(Base.ErrorWithValue). + fromType = fromType.GetType(Type.ErrorWithValue). GetGenericImplementation(arguments[1].ReturnType); return arguments; } private static IReadOnlyList NormalizeTypeArguments(Body body, Type fromType, IReadOnlyList arguments) => - fromType.Name == Base.Type && arguments.Count == 1 + fromType.Name == nameof(Type) && arguments.Count == 1 ? [ - arguments[0].ReturnType.Name == Base.Text - ? new Value(body.Method.GetType(Base.Name), ((Value)arguments[0]).Data) + arguments[0].ReturnType.IsText + ? new Value(body.Method.GetType(nameof(Type.Name)), ((Value)arguments[0]).Data) : arguments[0], new Text(body.Method, body.Method.Type.Package.FullName) ] @@ -211,10 +221,10 @@ private static IReadOnlyList CreateStacktraces(Body body) => [CreateStacktrace(body)]; private static Expression CreateStacktrace(Body body) => - CreateFromMethodCall(body, body.Method.GetType(Base.Stacktrace), [ - CreateFromMethodCall(body, body.Method.GetType(Base.Method), [ - new Value(body.Method.GetType(Base.Name), body.Method.Name), - CreateFromMethodCall(body, body.Method.GetType(Base.Type), + CreateFromMethodCall(body, body.Method.GetType(Type.Stacktrace), [ + CreateFromMethodCall(body, body.Method.GetType(nameof(Method)), [ + new Value(body.Method.GetType(nameof(Type.Name)), body.Method.Name), + CreateFromMethodCall(body, body.Method.GetType(nameof(Type)), [new Text(body.Method, body.Method.Type.Name)]) ]), new Text(body.Method, body.Method.Type.FilePath), @@ -233,18 +243,36 @@ Instance is not null ? (Instance is Binary ? $"({Instance})" : $"{Instance}") + $".{Method.Name}{Arguments.ToBrackets()}" - : ReturnType is GenericTypeImplementation { Generic.Name: Base.ErrorWithValue } + : ReturnType is GenericTypeImplementation { Generic.Name: Type.ErrorWithValue } ? Arguments[0] + "(" + Arguments[1] + ")" - : ReturnType.Name == Base.Error - ? Base.Error + : ReturnType.IsError + ? Type.Error : Method.Name == Method.From && - ReturnType is GenericTypeImplementation { Generic.Name: Base.Dictionary } + ReturnType is GenericTypeImplementation { Generic.Name: Type.Dictionary } ? FormatDictionaryConstructor() : $"{GetProperMethodName()}{Arguments.ToBrackets()}"; + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is MethodCall mc && other.GetType() == GetType() && + Method.IsSameMethodNameReturnTypeAndParameters(mc.Method) && + Equals(Instance, mc.Instance) && ArgumentsEqual(mc.Arguments)); + + private bool ArgumentsEqual(IReadOnlyList otherArguments) + { + if (Arguments.Count != otherArguments.Count) + return false; //ncrunch: no coverage + for (var i = 0; i < Arguments.Count; i++) + if (!Arguments[i].Equals(otherArguments[i])) + return false; //ncrunch: no coverage + return true; + } + + public override int GetHashCode() => Method.GetHashCode() ^ (Instance?.GetHashCode() ?? 0); + private string FormatDictionaryConstructor() => - Arguments.Count == 1 && Arguments[0] is List list - ? Base.Dictionary + (list.Values.All(value => value is List) + Arguments is [List list] + ? Type.Dictionary + (list.Values.All(value => value is List) ? list.Values.ToBrackets() : $"({list})") : throw new NotSupportedException("Invalid Dictionary arguments: " + Arguments.ToBrackets()); diff --git a/Strict.Expressions/MethodExpressionParser.cs b/Strict.Expressions/MethodExpressionParser.cs index 56d59744..f9dfc7c0 100644 --- a/Strict.Expressions/MethodExpressionParser.cs +++ b/Strict.Expressions/MethodExpressionParser.cs @@ -40,7 +40,7 @@ private static void CheckIfEmptyOrAny(Body body, ReadOnlySpan input) } private static bool IsExpressionTypeAny(ReadOnlySpan input) => - input.Equals(Base.Any, StringComparison.Ordinal) || input.StartsWith(Base.Any + "("); + input.Equals(Type.Any, StringComparison.Ordinal) || input.StartsWith(Type.Any + "("); private Expression TryParseCommon(Body body, ReadOnlySpan input, bool makeMutable) => Boolean.TryParse(body, input) ?? Text.TryParse(body, input) ?? @@ -58,20 +58,29 @@ private Expression TryParseCommon(Body body, ReadOnlySpan input, bool make return null; var valueVar = body.FindVariable(Type.ValueLowercase.AsSpan()); if (valueVar == null) - return null; + return null; //ncrunch: no coverage var inputName = input.ToString(); var valueType = valueVar.Type; var valueCall = new VariableCall(valueVar, body.CurrentFileLineNumber); var method = valueType.FindMethod(inputName, []); if (method != null) - return new MethodCall(method, valueCall, [], null, body.CurrentFileLineNumber); - var member = valueType.Members.FirstOrDefault(m => - m.Name.Equals(inputName, StringComparison.Ordinal)); + return new MethodCall(method, valueCall, [], null, body.CurrentFileLineNumber); //ncrunch: no coverage + var member = FindMember(valueType, inputName); return member != null ? new MemberCall(valueCall, member, body.CurrentFileLineNumber) : null; } + private static Member? FindMember(Type valueType, string inputName) + { + for (var index = 0; index < valueType.Members.Count; index++) + //ncrunch: no coverage start + if (valueType.Members[index].Name.Equals(inputName, StringComparison.Ordinal)) + return valueType.Members[index]; + //ncrunch: no coverage end + return null; + } + private static Expression? TryParseErrorOrTextOrListOrConditionalExpression(Body body, ReadOnlySpan input, bool makeMutable) => input[0] == '"' && input[^1] == '"' && MemoryExtensions.Count(input, '"') == 2 @@ -107,10 +116,10 @@ private Expression TryParseMethodOrMember(Body body, ReadOnlySpan input) throw new UnknownExpression(body, input[postfix.Output.Peek()].ToString() + " in " + inputText); } - +#if DEBUG private sealed class GeneratedBinaryExpressionDoesNotMatchInputExactly(Body body, Expression binary, string inputText) : ParsingFailed(body, binary + ", inputText=" + inputText); //ncrunch: no coverage - +#endif private Expression ParseMethodCallWithArguments(Body body, ReadOnlySpan input, ShuntingYard postfix) { @@ -289,8 +298,7 @@ private static Exception CheckErrorTypeAndThrowException(Body body, ReadOnlySpan // ReSharper disable once TooManyArguments private Expression? TryVariableOrValueOrParameterOrMemberOrMethodCall(Context context, - Expression? instance, Body body, ReadOnlySpan input, - IReadOnlyList arguments) + Expression? instance, Body body, ReadOnlySpan input, IReadOnlyList arguments) { var type = context as Type ?? body.Method.Type; return !input.IsWord() && !input.Contains(' ') && !input.Contains('(') @@ -354,10 +362,10 @@ private static Exception CheckErrorTypeAndThrowException(Body body, ReadOnlySpan Expression? instance, ReadOnlySpan input) { if (!input.Equals(Type.ElementsLowercase, StringComparison.Ordinal) || - type is not GenericTypeImplementation { Generic.Name: Base.Dictionary }) + type is not GenericTypeImplementation { Generic.Name: Type.Dictionary }) return null; var listMember = type.Members.FirstOrDefault(member => - member.Type.Name.StartsWith(Base.List, StringComparison.Ordinal)); + member.Type.Name.StartsWith(Type.List, StringComparison.Ordinal)); if (listMember == null) return null; //ncrunch: no coverage var keyword = listMember.IsConstant @@ -548,6 +556,6 @@ private sealed class InvalidSingleTokenExpression(Body body, string message) : ParsingFailed(body, message); //ncrunch: no coverage public sealed class InvalidArgumentItIsNotMethodOrListCall(Body body, - Expression variable, IEnumerable arguments) - : ParsingFailed(body, arguments.ToWordList(), variable.ReturnType); + Expression variable, IReadOnlyList arguments) + : ParsingFailed(body, string.Join(", ", arguments), variable.ReturnType); } \ No newline at end of file diff --git a/Strict.Expressions/MutableReassignment.cs b/Strict.Expressions/MutableReassignment.cs index 69be3364..341baa56 100644 --- a/Strict.Expressions/MutableReassignment.cs +++ b/Strict.Expressions/MutableReassignment.cs @@ -1,4 +1,4 @@ -using Strict.Language; +using Strict.Language; namespace Strict.Expressions; @@ -44,10 +44,17 @@ private static Expression TryParseReassignment(Body body, ReadOnlySpan lin return new MutableReassignment(body, expression, newExpression); } - public override bool IsConstant => false; - public override string ToString() => Name + " = " + Value; - public sealed class ValueTypeNotMatchingWithAssignmentType(Body body, string currentValueType, string newValueType) : ParsingFailed(body, $"Cannot assign {newValueType} value type to {currentValueType} member or variable"); + + public override bool IsConstant => false; + public override string ToString() => Name + " = " + Value; + //ncrunch: no coverage start + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is MutableReassignment mr && Name == mr.Name && + Target.Equals(mr.Target) && Value.Equals(mr.Value)); + + public override int GetHashCode() => Name.GetHashCode() ^ Value.GetHashCode(); } \ No newline at end of file diff --git a/Strict.Expressions/Number.cs b/Strict.Expressions/Number.cs index d09fc93d..4078724f 100644 --- a/Strict.Expressions/Number.cs +++ b/Strict.Expressions/Number.cs @@ -1,13 +1,11 @@ -using System.Globalization; using Strict.Language; +using Type = Strict.Language.Type; namespace Strict.Expressions; public sealed class Number(Context context, double value, int lineNumber = 0) - : Value(context.GetType(Base.Number), value, lineNumber) + : Value(context.GetType(Type.Number), value, lineNumber) { - public override string ToString() => ((double)Data).ToString(CultureInfo.InvariantCulture); - public static Expression? TryParse(Body body, ReadOnlySpan line) => line.TryParseNumber(out var number) ? new Number(body.Method, number, body.CurrentFileLineNumber) diff --git a/Strict.Expressions/ParameterCall.cs b/Strict.Expressions/ParameterCall.cs index a847144b..6cd2fa7c 100644 --- a/Strict.Expressions/ParameterCall.cs +++ b/Strict.Expressions/ParameterCall.cs @@ -1,4 +1,4 @@ -using Strict.Language; +using Strict.Language; namespace Strict.Expressions; @@ -17,4 +17,10 @@ public sealed class ParameterCall(Parameter parameter, int lineNumber = 0) } public override string ToString() => Parameter.Name; + //ncrunch: no coverage start + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is ParameterCall pc && Parameter.Name == pc.Parameter.Name && + Parameter.Type == pc.Parameter.Type); + public override int GetHashCode() => Parameter.GetHashCode(); } \ No newline at end of file diff --git a/Strict.Expressions/PhraseTokenizer.cs b/Strict.Expressions/PhraseTokenizer.cs index 75e6a198..34cdf467 100644 --- a/Strict.Expressions/PhraseTokenizer.cs +++ b/Strict.Expressions/PhraseTokenizer.cs @@ -5,7 +5,7 @@ namespace Strict.Expressions; /// /// Phrases are any expressions containing spaces (if not, they are just single expressions and /// don't need any tokenizing). They could come from a full line of code, conditions of ifs, the -/// right part of assignments or method call arguments. Optimized for speed and memory efficiency +/// right part of assignments, or method call arguments. Optimized for speed and memory efficiency /// (no new), no memory is allocated except for the check if we are in a list or just grouping. /// //ncrunch: no coverage start, for better performance diff --git a/Strict.Expressions/SelectorIf.cs b/Strict.Expressions/SelectorIf.cs index 34d86a8e..de53088e 100644 --- a/Strict.Expressions/SelectorIf.cs +++ b/Strict.Expressions/SelectorIf.cs @@ -67,4 +67,22 @@ private static Type GetMatchingType(Type thenType, Type? elseType, Body? bodyFor bodyForErrorMessage ?? new Body(thenType.Methods[0]), thenType, elseType); private const string ThenSeparator = " then "; + + //ncrunch: no coverage start + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is SelectorIf si && Selector.Equals(si.Selector) && + Cases.Count == si.Cases.Count && CasesEqual(si) && + (OptionalElse?.Equals(si.OptionalElse) ?? si.OptionalElse == null)); + + private bool CasesEqual(SelectorIf other) + { + for (var i = 0; i < Cases.Count; i++) + if (!Cases[i].Pattern.Equals(other.Cases[i].Pattern) || + !Cases[i].Then.Equals(other.Cases[i].Then)) + return false; + return true; + } + + public override int GetHashCode() => Selector.GetHashCode() ^ Cases.Count; } \ No newline at end of file diff --git a/Strict.Expressions/Text.cs b/Strict.Expressions/Text.cs index f701a066..9692869d 100644 --- a/Strict.Expressions/Text.cs +++ b/Strict.Expressions/Text.cs @@ -1,9 +1,10 @@ using Strict.Language; +using Type = Strict.Language.Type; namespace Strict.Expressions; public sealed class Text(Context context, string value, int lineNumber = 0) - : Value(context.GetType(Base.Text), value, lineNumber) + : Value(context.GetType(Type.Text), value, lineNumber) { /// /// Text must start and end with a double quote, only called for input that does not contain any diff --git a/Strict.Expressions/To.cs b/Strict.Expressions/To.cs index 52d3e1dc..e307bfe0 100644 --- a/Strict.Expressions/To.cs +++ b/Strict.Expressions/To.cs @@ -46,4 +46,12 @@ public sealed class ConversionTypeNotFound(Body body, string typeName) public sealed class ConversionTypeIsIncompatible(Body body, string message, Type type) : ParsingFailed(body, message, type); + + //ncrunch: no coverage start + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is To to && Method.IsSameMethodNameReturnTypeAndParameters(to.Method) && + Equals(Instance, to.Instance) && ConversionType == to.ConversionType); + + public override int GetHashCode() => Method.GetHashCode() ^ ConversionType.GetHashCode(); } \ No newline at end of file diff --git a/Strict.Expressions/TypeComparison.cs b/Strict.Expressions/TypeComparison.cs index bf5ab949..ccfaeb0f 100644 --- a/Strict.Expressions/TypeComparison.cs +++ b/Strict.Expressions/TypeComparison.cs @@ -1,4 +1,4 @@ -using Strict.Language; +using Strict.Language; using Type = Strict.Language.Type; namespace Strict.Expressions; @@ -25,7 +25,14 @@ public static Expression Parse(Body body, ReadOnlySpan input, Range nextTo ? body.ReturnType.FindType(input[nextTokenRange].ToString()) : null; return foundType != null - ? new TypeComparison(body.Method.GetType(Base.Type), foundType, body.CurrentFileLineNumber) + ? new TypeComparison(body.Method.GetType(nameof(Type)), foundType, + body.CurrentFileLineNumber) : body.Method.ParseExpression(body, input[nextTokenRange]); } + + //ncrunch: no coverage start + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is TypeComparison tc && TargetType == tc.TargetType); + public override int GetHashCode() => TargetType.GetHashCode(); } \ No newline at end of file diff --git a/Strict.Expressions/Value.cs b/Strict.Expressions/Value.cs index 547f2942..de87c759 100644 --- a/Strict.Expressions/Value.cs +++ b/Strict.Expressions/Value.cs @@ -6,28 +6,27 @@ namespace Strict.Expressions; /// /// Any expression with a fixed value, often optimized from all known code trees. Mostly used as /// parameters and assignment values via the derived classes , -/// or . -/// All expressions have a ReturnType and many expressions contains a like -/// or indirectly as parts of a expression. -/// For generic values like the Data contains the generic type used. +/// or . Already optimized for use in HighLevelRuntime. /// -public class Value(Type valueType, object data, int lineNumber = 0, bool isMutable = false) +public class Value(Type valueType, ValueInstance data, int lineNumber = 0, bool isMutable = false) : ConcreteExpression(valueType, lineNumber, isMutable) { - public object Data { get; } = data; + protected Value(Type valueType, bool value, int lineNumber = 0, bool isMutable = false) + : this(valueType, new ValueInstance(valueType, value), lineNumber, isMutable) { } - public override string ToString() => - Data switch - { - string => "\"" + Data + "\"", - double doubleData => doubleData.ToString("0.0"), - _ => Data.ToString()! - }; + protected Value(Type valueType, double value, int lineNumber = 0, bool isMutable = false) + : this(valueType, new ValueInstance(valueType, value), lineNumber, isMutable) { } - public override bool IsConstant => true; + public Value(Type valueType, string text, int lineNumber = 0, bool isMutable = false) + : this(valueType, new ValueInstance(text), lineNumber, isMutable) { } - public override bool Equals(Expression? other) => - other is Value v && EqualsExtensions.AreEqual(Data, v.Data); + protected Value(Type valueType, List items, int lineNumber = 0, + bool isMutable = false) : this(valueType, new ValueInstance(valueType, items), lineNumber, + isMutable) { } - public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Data); + public ValueInstance Data { get; } = data; + public override string ToString() => Data.ToExpressionCodeString(true); + public override bool IsConstant => true; + public override bool Equals(Expression? other) => other is Value v && Data.Equals(v.Data); + public override int GetHashCode() => Data.GetHashCode(); } \ No newline at end of file diff --git a/Strict.Expressions/ValueDictionaryInstance.cs b/Strict.Expressions/ValueDictionaryInstance.cs new file mode 100644 index 00000000..107e1f99 --- /dev/null +++ b/Strict.Expressions/ValueDictionaryInstance.cs @@ -0,0 +1,27 @@ +using Type = Strict.Language.Type; + +namespace Strict.Expressions; + +public sealed class ValueDictionaryInstance(Type returnType, + Dictionary items) : IEquatable +{ + public readonly Type ReturnType = returnType; + public readonly Dictionary Items = items; + + public bool Equals(ValueDictionaryInstance? other) + { + if (ReferenceEquals(this, other)) + return true; //ncrunch: no coverage + if (other is null || Items.Count != other.Items.Count || + !other.ReturnType.IsSameOrCanBeUsedAs(ReturnType)) + return false; + foreach (var kvp in Items) + if (!other.Items.TryGetValue(kvp.Key, out var value) || !kvp.Value.Equals(value)) + return false; + return true; + } + + //ncrunch: no coverage start + public override bool Equals(object? other) => Equals(other as ValueDictionaryInstance); + public override int GetHashCode() => HashCode.Combine(ReturnType, Items); +} \ No newline at end of file diff --git a/Strict.Expressions/ValueInstance.cs b/Strict.Expressions/ValueInstance.cs new file mode 100644 index 00000000..cbeab9cc --- /dev/null +++ b/Strict.Expressions/ValueInstance.cs @@ -0,0 +1,417 @@ +using Strict.Language; +using Type = Strict.Language.Type; + +namespace Strict.Expressions; + +/// +/// Optimized for size, always just contains 2 values, a pointer to the type, string, list, +/// dictionary, or type instance and if it is a primitive (most common, most lines just return +/// None or True) the None, Boolean, or Number data. This is also +/// +public readonly struct ValueInstance : IEquatable +{ + public ValueInstance(Type noneReturnType) => value = noneReturnType; + /// + /// If number is TextId, value points to a string (only non-Mutable, for Mutable TypeId is used) + /// If number is ListId, value points to ValueListInstance (ReturnType and Items) + /// If number is DictionaryId, value points to ValueDictionaryInstance (ReturnType and Items) + /// If number is TypeId, then this points to a TypeValueInstance containing the ReturnType. + /// In all other cases this is a primitive (None, Boolean, Number), and value is the ReturnType. + /// + private readonly object value; + /// + /// Stores the value only if it is a None, Boolean, or Number. Otherwise use value below. + /// + internal readonly double number; + /// + /// These are all unsupported double values, which we don't allow or support. + /// + private const double TextId = -7.90897526e307; + private const double ListId = -7.81590825e307; + private const double DictionaryId = -7.719027815e307; + private const double TypeId = -7.657178621e307; + + public ValueInstance(Type booleanReturnType, bool isTrue) + { + value = booleanReturnType; + number = isTrue + ? 1 + : 0; + } + + public ValueInstance(Type numberReturnType, double setNumber) + { + if (setNumber is TextId or ListId or DictionaryId or TypeId) + throw new InvalidTypeValue(numberReturnType, setNumber); + value = numberReturnType; + number = setNumber; + } + + public sealed class InvalidTypeValue(Type returnType, object value) : ParsingFailed(returnType, + //ncrunch: no coverage start + 0, value switch + { + null => "null", + Expression => "Expression " + value + " needs to be evaluated!", + //ncrunch: no coverage end + _ => value + "" + } + " (" + value?.GetType() + ") for " + returnType.Name); + + public ValueInstance(string text) + { + value = text; + number = TextId; + } + + public ValueInstance(Type returnType, IReadOnlyList list) + { + value = new ValueListInstance(returnType, list); + number = ListId; + } + + public ValueInstance(Type returnType, Dictionary dictionary) + { + value = new ValueDictionaryInstance(returnType, dictionary); + number = DictionaryId; + } + + public ValueInstance(Type returnType, Dictionary members) + { + if (!returnType.IsMutable && (returnType.IsNumber || returnType.IsText || + returnType.IsCharacter || returnType.IsList || returnType.IsDictionary || + returnType.IsEnum || returnType.IsBoolean || returnType.IsNone)) + throw new ValueTypeInstanceShouldOnlyBeCreatedForComplexTypes(returnType); + value = new ValueTypeInstance(returnType, members); + number = TypeId; + } + + public class ValueTypeInstanceShouldOnlyBeCreatedForComplexTypes(Language.Type returnType) + : Exception(returnType.ToString()) { } + + /// + /// Used by ApplyMethodReturnTypeMutable and TryEvaluate to flip if this is a mutable or not. + /// + public ValueInstance(ValueInstance existingInstance, Type newType) + { + switch (existingInstance.number) + { + case TextId: + value = new ValueTypeInstance(newType, new Dictionary + { + { Type.Text, new ValueInstance((string)existingInstance.value) } + }); + number = TypeId; + break; + case ListId: + value = new ValueListInstance(newType, ((ValueListInstance)existingInstance.value).Items); + number = ListId; + break; + case DictionaryId: + value = new ValueDictionaryInstance(newType, ((ValueDictionaryInstance)existingInstance.value).Items); + number = DictionaryId; + break; + case TypeId: + var existingTypeInstance = (ValueTypeInstance)existingInstance.value; + if (!newType.IsMutable && existingTypeInstance.ReturnType.IsMutable && newType.IsText) + { + value = existingTypeInstance.Members[Type.Text].value; + number = TextId; + } + else + { + value = new ValueTypeInstance(newType, existingTypeInstance.Members); + number = TypeId; + } + break; + default: + value = newType; + number = existingInstance.number; + break; + } + } + + public bool IsType(Type type) => + number switch + { + TextId => type.IsText, + ListId => type == ((ValueListInstance)value).ReturnType, + DictionaryId => type == ((ValueDictionaryInstance)value).ReturnType, + TypeId => type == ((ValueTypeInstance)value).ReturnType, + _ => IsPrimitiveType(type) + }; + + public bool IsPrimitiveType(Type noneBoolOrNumberType) => value == noneBoolOrNumberType; + public bool IsText => number is TextId; + public string Text => (string)value; + public double Number => number; + public bool Boolean => number != 0; + public bool IsList => number is ListId; + public ValueListInstance List => (ValueListInstance)value; + public bool IsDictionary => number is DictionaryId; + public bool IsNumberLike(Type numberType) => + IsPrimitiveType(numberType) || + (!IsText && !IsList && !IsDictionary) && GetTypeExceptText().IsSameOrCanBeUsedAs(numberType); + + public bool IsSameOrCanBeUsedAs(Type otherType) => + number switch + { + TextId => otherType.IsText || otherType.IsList && + otherType is GenericTypeImplementation { ImplementationTypes: [{ IsCharacter: true }] }, + ListId => ((ValueListInstance)value).ReturnType.IsSameOrCanBeUsedAs(otherType), + DictionaryId => ((ValueDictionaryInstance)value).ReturnType.IsSameOrCanBeUsedAs(otherType), + TypeId => ((ValueTypeInstance)value).ReturnType.IsSameOrCanBeUsedAs(otherType), + _ => ((Type)value).IsSameOrCanBeUsedAs(otherType) + }; + + public bool IsValueTypeInstanceType => + number == TypeId && value is ValueTypeInstance { ReturnType.Name: nameof(Type) }; + + public ValueTypeInstance? TryGetValueTypeInstance() => + number == TypeId + ? (ValueTypeInstance)value + : null; + + /// + /// Special code to make the ValueInstance mutable if the method return type requires it (rare) + /// + public ValueInstance ApplyMethodReturnTypeMutable(Type methodReturnType) + { + var isInstanceMutable = IsMutable; + if (isInstanceMutable == methodReturnType.IsMutable) + return this; + if (!isInstanceMutable) + return IsSameOrCanBeUsedAs(methodReturnType.GetFirstImplementation()) + ? new ValueInstance(this, methodReturnType) + : this; + return GetTypeExceptText().GetFirstImplementation().IsSameOrCanBeUsedAs(methodReturnType) + ? new ValueInstance(this, methodReturnType) + : this; + } + + public Type GetTypeExceptText() => + number switch + { + ListId => ((ValueListInstance)value).ReturnType, + DictionaryId => ((ValueDictionaryInstance)value).ReturnType, + TypeId => ((ValueTypeInstance)value).ReturnType, + _ => (Type)value + }; + + public int GetIteratorLength() + { + if (number == ListId) + return ((ValueListInstance)value).Items.Count; + if (number == TextId) + return ((string)value).Length; + if (number == DictionaryId) + throw new IteratorNotSupported(this); + if (number == TypeId) + { + var typeInstance = (ValueTypeInstance)value; + if (typeInstance.ReturnType.IsList && typeInstance.Members.TryGetValue(Type.Text, out var textMember)) + return textMember.Text.Length; + if (typeInstance.Members.TryGetValue("keysAndValues", out var elementsMember) && elementsMember.IsList) + return elementsMember.List.Items.Count; + throw new IteratorNotSupported(this); + } + return (int)number; + } + + public Type GetIteratorType() => ((ValueListInstance)value).ReturnType.GetFirstImplementation(); + + public ValueInstance GetIteratorValue(Type charTypeIfNeeded, int index) => + number switch + { + TextId => new ValueInstance(charTypeIfNeeded, ((string)value)[index]), + ListId => ((ValueListInstance)value).Items[index], + TypeId when ((ValueTypeInstance)value).ReturnType.IsList && + ((ValueTypeInstance)value).Members.TryGetValue(Type.Text, out var textMember) => + new ValueInstance(charTypeIfNeeded, textMember.Text[index]), + TypeId when ((ValueTypeInstance)value).Members.TryGetValue("elements", out var elementsMember) && + elementsMember.IsList => elementsMember.List.Items[index], + _ => throw new IteratorNotSupported(this) + }; + + public class IteratorNotSupported(ValueInstance instance) : Exception(instance.ToString()); + + public Dictionary GetDictionaryItems() => + ((ValueDictionaryInstance)value).Items; + + public bool IsMutable => + number switch + { + TextId => false, + ListId => ((ValueListInstance)value).ReturnType.IsMutable, + DictionaryId => ((ValueDictionaryInstance)value).ReturnType.IsMutable, + TypeId => ((ValueTypeInstance)value).ReturnType.IsMutable, + _ => ((Type)value).IsMutable + }; + public bool IsError => number == TypeId && ((ValueTypeInstance)value).ReturnType.IsError; + public override string ToString() => GetTypeName() + ": " + ToExpressionCodeString(true); + + private string GetTypeName() => + number switch + { + TextId => Type.Text, + ListId => ((ValueListInstance)value).ReturnType.Name, + DictionaryId => ((ValueDictionaryInstance)value).ReturnType.Name, + TypeId => ((ValueTypeInstance)value).ReturnType.Name, + _ => ((Type)value).Name + }; + + public string ToExpressionCodeString(bool escapeText = false) + { + ToExpressionCodeStringCalls++; + if (escapeText) + ToExpressionCodeStringEscapedCalls++; + return number switch + { + TextId => escapeText + ? "\"" + EscapeText((string)value) + "\"" + : (string)value, + ListId => BuildListString(((ValueListInstance)value).Items, escapeText), + DictionaryId => BuildDictionaryString(((ValueDictionaryInstance)value).Items, escapeText), + TypeId => ToTypeExpressionCodeString(), + _ => GetPrimitiveCodeString((Type)value) + }; + } + + public static int ToExpressionCodeStringTypeIdCalls; + + private string ToTypeExpressionCodeString() + { + ToExpressionCodeStringTypeIdCalls++; + return ((ValueTypeInstance)value).ToString(); + } + + public static int ToExpressionCodeStringCalls = 0; + public static int ToExpressionCodeStringEscapedCalls; + private static string EscapeText(string s) => s.Replace("\\", @"\\").Replace("\"", "\\\""); + + private static string BuildListString(IReadOnlyList items, bool escapeText) + { + if (items.Count == 0) + return ""; + if (items.Count == 1) + return items[0].ToExpressionCodeString(escapeText); + var parts = new string[items.Count]; + for (var i = 0; i < items.Count; i++) + parts[i] = items[i].ToExpressionCodeString(escapeText); + return string.Join(", ", parts); + } + + private static string BuildDictionaryString(Dictionary items, + bool escapeText) + { + if (items.Count == 0) + return ""; + var parts = new string[items.Count]; + var i = 0; + foreach (var kv in items) + parts[i++] = "(" + kv.Key.ToExpressionCodeString(escapeText) + ", " + + kv.Value.ToExpressionCodeString(escapeText) + ")"; + return string.Join(", ", parts); + } + + private string GetPrimitiveCodeString(Type primitiveType) + { + GetPrimitiveCodeStringCalls++; + if (primitiveType.IsBoolean) + return number == 0 + ? "false" + : "true"; + if (primitiveType.IsNone) + return Type.None; + if (primitiveType.IsNumber) + return GetCachedNumberString(); + if (primitiveType.IsCharacter) + return GetCachedCharString(); + GetPrimitiveCodeStringCallsNonNumberBooleanChar++; + return primitiveType.IsMutable + // ReSharper disable once TailRecursiveCall + ? GetPrimitiveCodeString(primitiveType.GetFirstImplementation()) + : throw new NotSupportedException(primitiveType.ToString()); + } + + public static int GetPrimitiveCodeStringCalls = 0; + public static int GetPrimitiveCodeStringCallsNonNumberBooleanChar = 0; + + public string GetCachedNumberString() + { + if (double.IsInteger(number)) + { + var intValue = (int)number; + if ((uint)intValue < (uint)CachedIntegerStrings.Length) + return CachedIntegerStrings[intValue]; + if (intValue == -1) + return "-1"; + } + return number.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + private static readonly string[] CachedIntegerStrings = CreateIntegerStringCache(); + + private static string[] CreateIntegerStringCache() + { + var cache = new string[101]; + for (var i = 0; i < cache.Length; i++) + cache[i] = i.ToString(System.Globalization.CultureInfo.InvariantCulture); + return cache; + } + + private string GetCachedCharString() + { + var c = (char)number; + return c < 128 + ? CachedAsciiCharStrings[c] + : c.ToString(); + } + + private static readonly string[] CachedAsciiCharStrings = CreateAsciiCharCache(); + + private static string[] CreateAsciiCharCache() + { + var cache = new string[128]; + for (var i = 0; i < cache.Length; i++) + cache[i] = ((char)i).ToString(); + return cache; + } + + /// + /// Equals is a bit more complex as we need to handle all different kinds of ValueInstances we + /// could have, either primary, text, list, dictionary, or type. Or any of those mixed. + /// + public bool Equals(ValueInstance other) + { + EqualsCalls++; + if (number == other.number && value == other.value) + return true; + if (number == TypeId) + { + var instance = (ValueTypeInstance)value; + if (other.number == TypeId) + return instance.Equals((ValueTypeInstance)other.value); + if (other.number != ListId && other.number != DictionaryId && other.number != TextId && + instance.Members.TryGetValue("number", out var numberMember) && + numberMember.number == other.number) + return true; + } + else if (other.number == TypeId && number != ListId && number != DictionaryId && + number != TextId && + ((ValueTypeInstance)other.value).Members.TryGetValue("number", + out var otherNumberMember) && otherNumberMember.number == number) + return true; + if (number != other.number) + return false; + if (number == TextId) + return (string)value == (string)other.value; + if (number == ListId) + return ((ValueListInstance)value).Equals((ValueListInstance)other.value); + return number == DictionaryId + ? ((ValueDictionaryInstance)value).Equals((ValueDictionaryInstance)other.value) + : ((Type)other.value).IsSameOrCanBeUsedAs((Type)value); + } + + public static int EqualsCalls = 0; + public override int GetHashCode() => HashCode.Combine(number, value); +} \ No newline at end of file diff --git a/Strict.Expressions/ValueListInstance.cs b/Strict.Expressions/ValueListInstance.cs new file mode 100644 index 00000000..be568de9 --- /dev/null +++ b/Strict.Expressions/ValueListInstance.cs @@ -0,0 +1,18 @@ +using Type = Strict.Language.Type; + +namespace Strict.Expressions; + +public sealed class ValueListInstance(Type returnType, IReadOnlyList items) : + IEquatable +{ + public readonly Type ReturnType = returnType; + public readonly IReadOnlyList Items = items; + + public bool Equals(ValueListInstance? other) => + other is not null && (ReferenceEquals(this, other) || + other.ReturnType.IsSameOrCanBeUsedAs(ReturnType) && Items.SequenceEqual(other.Items)); + + //ncrunch: no coverage start + public override bool Equals(object? other) => Equals(other as ValueListInstance); + public override int GetHashCode() => HashCode.Combine(ReturnType, Items); +} \ No newline at end of file diff --git a/Strict.Expressions/ValueTypeInstance.cs b/Strict.Expressions/ValueTypeInstance.cs new file mode 100644 index 00000000..d65d37ea --- /dev/null +++ b/Strict.Expressions/ValueTypeInstance.cs @@ -0,0 +1,19 @@ +using Strict.Language; +using Type = Strict.Language.Type; + +namespace Strict.Expressions; + +public sealed class ValueTypeInstance(Type returnType, + Dictionary members) : IEquatable +{ + public readonly Type ReturnType = returnType; + public readonly Dictionary Members = members; + + public bool Equals(ValueTypeInstance? other) => + other is not null && (ReferenceEquals(this, other) || + other.ReturnType.IsSameOrCanBeUsedAs(ReturnType) && Members.Count == other.Members.Count && + Members.All(kvp => + other.Members.TryGetValue(kvp.Key, out var value) && kvp.Value.Equals(value))); + + public override string ToString() => ReturnType + ": " + Members.DictionaryToWordList(); +} \ No newline at end of file diff --git a/Strict.Expressions/VariableCall.cs b/Strict.Expressions/VariableCall.cs index 12be60c3..623a1409 100644 --- a/Strict.Expressions/VariableCall.cs +++ b/Strict.Expressions/VariableCall.cs @@ -1,4 +1,4 @@ -using Strict.Language; +using Strict.Language; namespace Strict.Expressions; @@ -15,5 +15,10 @@ public sealed class VariableCall(Variable variable, int lineNumber = 0) public Variable Variable { get; } = variable; public override string ToString() => Variable.Name; + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is VariableCall vc && Variable.Name == vc.Variable.Name && + Variable.Type == vc.Variable.Type); + public override int GetHashCode() => Variable.GetHashCode(); //ncrunch: no coverage public override bool IsConstant => Variable.InitialValue.IsConstant && !Variable.IsMutable; } \ No newline at end of file diff --git a/Strict.Grammar.Tests/GrammarTests.cs b/Strict.Grammar.Tests/GrammarTests.cs index c90d566a..13fa8aca 100644 --- a/Strict.Grammar.Tests/GrammarTests.cs +++ b/Strict.Grammar.Tests/GrammarTests.cs @@ -74,18 +74,14 @@ private static string GetErrorDetails(string file, GrammarMatch checkedGrammar) [Test] public void CheckAllBaseFiles() { - var basePath = Directory.Exists(DefaultStrictBasePath) - ? DefaultStrictBasePath - : Path.Combine(FindSolutionPath(), "Strict.Base"); - foreach (var file in Directory.GetFiles(basePath, "*.strict")) + var localPath = Repositories.GetLocalDevelopmentPath(Repositories.StrictOrg, nameof(Strict)); + var basePath = Directory.Exists(localPath) + ? localPath + : Path.Combine(Directory.GetCurrentDirectory(), "..", "..", ".."); + foreach (var file in Directory.GetFiles(basePath, "*" + Language.Type.Extension)) { var result = BuildGrammar().Match(File.ReadAllText(file).Replace("\r\n", "\n") + "\n"); Assert.That(result.Success, Is.True, file + ": " + GetErrorDetails("", result)); } } - - private const string DefaultStrictBasePath = Repositories.StrictDevelopmentFolderPrefix + "Base"; - - private static string FindSolutionPath() => - Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", ".."); //ncrunch: no coverage } \ No newline at end of file diff --git a/Strict.Grammar.Tests/TestResults/GrammarTests.trx b/Strict.Grammar.Tests/TestResults/GrammarTests.trx deleted file mode 100644 index 3173ee71..00000000 --- a/Strict.Grammar.Tests/TestResults/GrammarTests.trx +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - C:\code\GitHub\strict-lang\Strict.Base\Boolean.strict: Index=349, Line=16, Context=" false) - >>>(value and" -Expected: -LF: Literal: ' -' -TAB: Literal: ' ' -member: (Literal: 'has', variable: (NAME: Repeat: (Char: 0x41 to 0x5a | Char: 0x61 to 0x7a), Optional: type: (NAME, Optional: (Literal: '(', variable, Repeat: (Literal: ',', variable), Literal: ')'))), Optional: (Literal: 'with', expression: ((constant: (Literal: 'true' | Literal: 'false' | NUMBER: (Repeat: Char: 0x30 to 0x39, Optional: (Literal: '.', Repeat: Char: 0x30 to 0x39)) | STRING: (Literal: '"', Repeat: (STRINGCHAR: Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-' | TAB: Literal: ' ' | Literal: '\'), Literal: '"')) | group: (Literal: '(', expression, Repeat: (Literal: ',', expression), Literal: ')') | unary_exp: (unary_op: (Literal: '-' | Literal: 'not'), expression) | return: (Literal: 'return', expression) | mutable: (Literal: 'mutable', NAME, Literal: '=', expression) | if: (Literal: 'if', expression, LF: Literal: ' -', block: Repeat: (Repeat: TAB, expression, Optional: LF), Optional: (Literal: 'else', LF, block)) | for: (Literal: 'for', Optional: (reference: NAME, Literal: 'in'), expression, LF, block) | member | assignment: (Optional: Literal: 'constant', Optional: Literal: 'mutable', Optional: Literal: 'let', NAME, Literal: '=', expression) | variablecall: (NAME, Optional: (Literal: '(', Optional: arguments: (argument: expression, Repeat: (Literal: ',', argument)), Literal: ')'))), Repeat: ((Literal: '.', methodcall: (methodname: (NAME | binary_op: (Literal: '+' | Literal: '-' | Literal: '*' | Literal: '/' | Literal: '%' | Literal: '^' | Literal: '<' | Literal: '<=' | Literal: '>' | Literal: '>=' | Literal: 'in' | Literal: 'is' | Literal: 'is not' | Literal: 'is not in' | Literal: 'and' | Literal: 'or' | Literal: 'xor') | unary_op | conversion: (Literal: 'from' | Literal: 'to')), Optional: (Literal: '(', Optional: arguments, Literal: ')'))) | (conversion, variablecall) | (binary_op, expression) | (Literal: 'then', expression, (Literal: 'else' | Literal: ':'), expression)))), Optional: LF) -NAME: Repeat: (Char: 0x41 to 0x5a | Char: 0x61 to 0x7a) -binary_op: (Literal: '+' | Literal: '-' | Literal: '*' | Literal: '/' | Literal: '%' | Literal: '^' | Literal: '<' | Literal: '<=' | Literal: '>' | Literal: '>=' | Literal: 'in' | Literal: 'is' | Literal: 'is not' | Literal: 'is not in' | Literal: 'and' | Literal: 'or' | Literal: 'xor') -unary_op: (Literal: '-' | Literal: 'not') -conversion: (Literal: 'from' | Literal: 'to') -methodname: (NAME: Repeat: (Char: 0x41 to 0x5a | Char: 0x61 to 0x7a) | binary_op: (Literal: '+' | Literal: '-' | Literal: '*' | Literal: '/' | Literal: '%' | Literal: '^' | Literal: '<' | Literal: '<=' | Literal: '>' | Literal: '>=' | Literal: 'in' | Literal: 'is' | Literal: 'is not' | Literal: 'is not in' | Literal: 'and' | Literal: 'or' | Literal: 'xor') | unary_op: (Literal: '-' | Literal: 'not') | conversion: (Literal: 'from' | Literal: 'to')) -method: (methodname: (NAME: Repeat: (Char: 0x41 to 0x5a | Char: 0x61 to 0x7a) | binary_op: (Literal: '+' | Literal: '-' | Literal: '*' | Literal: '/' | Literal: '%' | Literal: '^' | Literal: '<' | Literal: '<=' | Literal: '>' | Literal: '>=' | Literal: 'in' | Literal: 'is' | Literal: 'is not' | Literal: 'is not in' | Literal: 'and' | Literal: 'or' | Literal: 'xor') | unary_op: (Literal: '-' | Literal: 'not') | conversion: (Literal: 'from' | Literal: 'to')), Optional: (Literal: '(', parameters: (parameter: (NAME, Optional: type: (NAME, Optional: (Literal: '(', variable: (NAME, Optional: type), Repeat: (Literal: ',', variable), Literal: ')')), Optional: (Literal: '=', expression: ((constant: (Literal: 'true' | Literal: 'false' | NUMBER: (Repeat: Char: 0x30 to 0x39, Optional: (Literal: '.', Repeat: Char: 0x30 to 0x39)) | STRING: (Literal: '"', Repeat: (STRINGCHAR: Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-' | TAB: Literal: ' ' | Literal: '\'), Literal: '"')) | group: (Literal: '(', expression, Repeat: (Literal: ',', expression), Literal: ')') | unary_exp: (unary_op, expression) | return: (Literal: 'return', expression) | mutable: (Literal: 'mutable', NAME, Literal: '=', expression) | if: (Literal: 'if', expression, LF: Literal: ' -', block: Repeat: (Repeat: TAB, expression, Optional: LF), Optional: (Literal: 'else', LF, block)) | for: (Literal: 'for', Optional: (reference: NAME, Literal: 'in'), expression, LF, block) | member: (Literal: 'has', variable, Optional: (Literal: 'with', expression), Optional: LF) | assignment: (Optional: Literal: 'constant', Optional: Literal: 'mutable', Optional: Literal: 'let', NAME, Literal: '=', expression) | variablecall: (NAME, Optional: (Literal: '(', Optional: arguments: (argument: expression, Repeat: (Literal: ',', argument)), Literal: ')'))), Repeat: ((Literal: '.', methodcall: (methodname, Optional: (Literal: '(', Optional: arguments, Literal: ')'))) | (conversion, variablecall) | (binary_op, expression) | (Literal: 'then', expression, (Literal: 'else' | Literal: ':'), expression))))), Repeat: (Literal: ',', parameter)), Literal: ')'), Optional: type, Optional: LF, Optional: block) -assignment: (Optional: Literal: 'constant', Optional: Literal: 'mutable', Optional: Literal: 'let', NAME: Repeat: (Char: 0x41 to 0x5a | Char: 0x61 to 0x7a), Literal: '=', expression: ((constant: (Literal: 'true' | Literal: 'false' | NUMBER: (Repeat: Char: 0x30 to 0x39, Optional: (Literal: '.', Repeat: Char: 0x30 to 0x39)) | STRING: (Literal: '"', Repeat: (STRINGCHAR: Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-' | TAB: Literal: ' ' | Literal: '\'), Literal: '"')) | group: (Literal: '(', expression, Repeat: (Literal: ',', expression), Literal: ')') | unary_exp: (unary_op: (Literal: '-' | Literal: 'not'), expression) | return: (Literal: 'return', expression) | mutable: (Literal: 'mutable', NAME, Literal: '=', expression) | if: (Literal: 'if', expression, LF: Literal: ' -', block: Repeat: (Repeat: TAB, expression, Optional: LF), Optional: (Literal: 'else', LF, block)) | for: (Literal: 'for', Optional: (reference: NAME, Literal: 'in'), expression, LF, block) | member: (Literal: 'has', variable: (NAME, Optional: type: (NAME, Optional: (Literal: '(', variable, Repeat: (Literal: ',', variable), Literal: ')'))), Optional: (Literal: 'with', expression), Optional: LF) | assignment | variablecall: (NAME, Optional: (Literal: '(', Optional: arguments: (argument: expression, Repeat: (Literal: ',', argument)), Literal: ')'))), Repeat: ((Literal: '.', methodcall: (methodname: (NAME | binary_op: (Literal: '+' | Literal: '-' | Literal: '*' | Literal: '/' | Literal: '%' | Literal: '^' | Literal: '<' | Literal: '<=' | Literal: '>' | Literal: '>=' | Literal: 'in' | Literal: 'is' | Literal: 'is not' | Literal: 'is not in' | Literal: 'and' | Literal: 'or' | Literal: 'xor') | unary_op | conversion: (Literal: 'from' | Literal: 'to')), Optional: (Literal: '(', Optional: arguments, Literal: ')'))) | (conversion, variablecall) | (binary_op, expression) | (Literal: 'then', expression, (Literal: 'else' | Literal: ':'), expression)))) -Error Eto.Parse.UnaryParser: LF: Literal: ' -', Children: LF: Literal: ' -',Literal: ' -',Error Eto.Parse.UnaryParser: TAB: Literal: ' ', Children: TAB: Literal: ' ',Literal: ' ',Error Eto.Parse.UnaryParser: member: Sequence, Children: member: Sequence,Sequence,Repeat: Char: White Space,Char: White Space,Optional: LF: Literal: ' -',LF: Literal: ' -',Literal: ' -',Optional: Sequence,Sequence,expression: Sequence,Sequence,Repeat: Alternative,Alternative,Sequence,Alternative,Literal: ':',Literal: 'else',Literal: 'then',Sequence,binary_op: Alternative,Alternative,Literal: 'xor',Literal: 'or',Literal: 'and',Literal: 'is not in',Literal: 'is not',Literal: 'is',Literal: 'in',Literal: '>=',Literal: '>',Literal: '<=',Literal: '<',Literal: '^',Literal: '%',Literal: '/',Literal: '*',Literal: '-',Literal: '+',Sequence,variablecall: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Optional: arguments: Sequence,arguments: Sequence,Sequence,Repeat: Sequence,Sequence,argument: expression: Sequence,Literal: ',',Literal: '(',NAME: Repeat: Alternative,Repeat: Alternative,Alternative,Char: 0x61 to 0x7a,Char: 0x41 to 0x5a,conversion: Alternative,Alternative,Literal: 'to',Literal: 'from',Sequence,methodcall: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Optional: arguments: Sequence,Literal: '(',methodname: Alternative,Alternative,unary_op: Alternative,Alternative,Literal: 'not',Literal: '-',Literal: '.',Alternative,assignment: Sequence,Sequence,Literal: '=',Optional: Literal: 'let',Literal: 'let',Optional: Literal: 'mutable',Literal: 'mutable',Optional: Literal: 'constant',Literal: 'constant',for: Sequence,Sequence,block: Repeat: Sequence,Repeat: Sequence,Sequence,Optional: LF: Literal: ' -',Repeat: TAB: Literal: ' ',TAB: Literal: ' ',Literal: ' ',Optional: Sequence,Sequence,Literal: 'in',reference: NAME: Repeat: Alternative,Literal: 'for',if: Sequence,Sequence,Optional: Sequence,Sequence,Literal: 'else',Literal: 'if',mutable: Sequence,Sequence,Literal: '=',Literal: 'mutable',return: Sequence,Sequence,Literal: 'return',unary_exp: Sequence,Sequence,group: Sequence,Sequence,Literal: ')',Repeat: Sequence,Sequence,Literal: ',',Literal: '(',constant: Alternative,Alternative,STRING: Sequence,Sequence,Literal: '"',Repeat: Alternative,Alternative,Literal: '\',STRINGCHAR: Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-',Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-',Literal: '"',NUMBER: Sequence,Sequence,Optional: Sequence,Sequence,Repeat: Char: 0x30 to 0x39,Char: 0x30 to 0x39,Literal: '.',Repeat: Char: 0x30 to 0x39,Char: 0x30 to 0x39,Literal: 'false',Literal: 'true',Literal: 'with',variable: Sequence,Sequence,Optional: type: Sequence,type: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Repeat: Sequence,Sequence,Literal: ',',Literal: '(',Literal: 'has',Error Eto.Parse.UnaryParser: NAME: Repeat: Alternative, Children: NAME: Repeat: Alternative,Repeat: Alternative,Alternative,Char: 0x61 to 0x7a,Char: 0x41 to 0x5a,Error Eto.Parse.UnaryParser: binary_op: Alternative, Children: binary_op: Alternative,Alternative,Literal: 'xor',Literal: 'or',Literal: 'and',Literal: 'is not in',Literal: 'is not',Literal: 'is',Literal: 'in',Literal: '>=',Literal: '>',Literal: '<=',Literal: '<',Literal: '^',Literal: '%',Literal: '/',Literal: '*',Literal: '-',Literal: '+',Error Eto.Parse.UnaryParser: unary_op: Alternative, Children: unary_op: Alternative,Alternative,Literal: 'not',Literal: '-',Error Eto.Parse.UnaryParser: conversion: Alternative, Children: conversion: Alternative,Alternative,Literal: 'to',Literal: 'from',Error Eto.Parse.UnaryParser: methodname: Alternative, Children: methodname: Alternative,Alternative,conversion: Alternative,Alternative,Literal: 'to',Literal: 'from',unary_op: Alternative,Alternative,Literal: 'not',Literal: '-',binary_op: Alternative,Alternative,Literal: 'xor',Literal: 'or',Literal: 'and',Literal: 'is not in',Literal: 'is not',Literal: 'is',Literal: 'in',Literal: '>=',Literal: '>',Literal: '<=',Literal: '<',Literal: '^',Literal: '%',Literal: '/',Literal: '*',Literal: '-',Literal: '+',NAME: Repeat: Alternative,Repeat: Alternative,Alternative,Char: 0x61 to 0x7a,Char: 0x41 to 0x5a,Error Eto.Parse.UnaryParser: method: Sequence, Children: method: Sequence,Sequence,Repeat: Char: White Space,Char: White Space,Optional: block: Repeat: Sequence,block: Repeat: Sequence,Repeat: Sequence,Sequence,Optional: LF: Literal: ' -',LF: Literal: ' -',Literal: ' -',expression: Sequence,Sequence,Repeat: Alternative,Alternative,Sequence,Alternative,Literal: ':',Literal: 'else',Literal: 'then',Sequence,binary_op: Alternative,Alternative,Literal: 'xor',Literal: 'or',Literal: 'and',Literal: 'is not in',Literal: 'is not',Literal: 'is',Literal: 'in',Literal: '>=',Literal: '>',Literal: '<=',Literal: '<',Literal: '^',Literal: '%',Literal: '/',Literal: '*',Literal: '-',Literal: '+',Sequence,variablecall: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Optional: arguments: Sequence,arguments: Sequence,Sequence,Repeat: Sequence,Sequence,argument: expression: Sequence,Literal: ',',Literal: '(',NAME: Repeat: Alternative,Repeat: Alternative,Alternative,Char: 0x61 to 0x7a,Char: 0x41 to 0x5a,conversion: Alternative,Alternative,Literal: 'to',Literal: 'from',Sequence,methodcall: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Optional: arguments: Sequence,Literal: '(',methodname: Alternative,Alternative,unary_op: Alternative,Alternative,Literal: 'not',Literal: '-',Literal: '.',Alternative,assignment: Sequence,Sequence,Literal: '=',Optional: Literal: 'let',Literal: 'let',Optional: Literal: 'mutable',Literal: 'mutable',Optional: Literal: 'constant',Literal: 'constant',member: Sequence,Sequence,Optional: LF: Literal: ' -',Optional: Sequence,Sequence,Literal: 'with',variable: Sequence,Sequence,Optional: type: Sequence,type: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Repeat: Sequence,Sequence,Literal: ',',Literal: '(',Literal: 'has',for: Sequence,Sequence,Optional: Sequence,Sequence,Literal: 'in',reference: NAME: Repeat: Alternative,Literal: 'for',if: Sequence,Sequence,Optional: Sequence,Sequence,Literal: 'else',Literal: 'if',mutable: Sequence,Sequence,Literal: '=',Literal: 'mutable',return: Sequence,Sequence,Literal: 'return',unary_exp: Sequence,Sequence,group: Sequence,Sequence,Literal: ')',Repeat: Sequence,Sequence,Literal: ',',Literal: '(',constant: Alternative,Alternative,STRING: Sequence,Sequence,Literal: '"',Repeat: Alternative,Alternative,Literal: '\',TAB: Literal: ' ',Literal: ' ',STRINGCHAR: Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-',Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-',Literal: '"',NUMBER: Sequence,Sequence,Optional: Sequence,Sequence,Repeat: Char: 0x30 to 0x39,Char: 0x30 to 0x39,Literal: '.',Repeat: Char: 0x30 to 0x39,Char: 0x30 to 0x39,Literal: 'false',Literal: 'true',Repeat: TAB: Literal: ' ',Optional: LF: Literal: ' -',Optional: type: Sequence,Optional: Sequence,Sequence,Literal: ')',parameters: Sequence,Sequence,Repeat: Sequence,Sequence,parameter: Sequence,Sequence,Optional: Sequence,Sequence,Literal: '=',Optional: type: Sequence,Literal: ',',Literal: '(',Error Eto.Parse.UnaryParser: assignment: Sequence, Children: assignment: Sequence,Sequence,Repeat: Char: White Space,Char: White Space,expression: Sequence,Sequence,Repeat: Alternative,Alternative,Sequence,Alternative,Literal: ':',Literal: 'else',Literal: 'then',Sequence,binary_op: Alternative,Alternative,Literal: 'xor',Literal: 'or',Literal: 'and',Literal: 'is not in',Literal: 'is not',Literal: 'is',Literal: 'in',Literal: '>=',Literal: '>',Literal: '<=',Literal: '<',Literal: '^',Literal: '%',Literal: '/',Literal: '*',Literal: '-',Literal: '+',Sequence,variablecall: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Optional: arguments: Sequence,arguments: Sequence,Sequence,Repeat: Sequence,Sequence,argument: expression: Sequence,Literal: ',',Literal: '(',NAME: Repeat: Alternative,Repeat: Alternative,Alternative,Char: 0x61 to 0x7a,Char: 0x41 to 0x5a,conversion: Alternative,Alternative,Literal: 'to',Literal: 'from',Sequence,methodcall: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Optional: arguments: Sequence,Literal: '(',methodname: Alternative,Alternative,unary_op: Alternative,Alternative,Literal: 'not',Literal: '-',Literal: '.',Alternative,member: Sequence,Sequence,Optional: LF: Literal: ' -',LF: Literal: ' -',Literal: ' -',Optional: Sequence,Sequence,Literal: 'with',variable: Sequence,Sequence,Optional: type: Sequence,type: Sequence,Sequence,Optional: Sequence,Sequence,Literal: ')',Repeat: Sequence,Sequence,Literal: ',',Literal: '(',Literal: 'has',for: Sequence,Sequence,block: Repeat: Sequence,Repeat: Sequence,Sequence,Optional: LF: Literal: ' -',Repeat: TAB: Literal: ' ',TAB: Literal: ' ',Literal: ' ',Optional: Sequence,Sequence,Literal: 'in',reference: NAME: Repeat: Alternative,Literal: 'for',if: Sequence,Sequence,Optional: Sequence,Sequence,Literal: 'else',Literal: 'if',mutable: Sequence,Sequence,Literal: '=',Literal: 'mutable',return: Sequence,Sequence,Literal: 'return',unary_exp: Sequence,Sequence,group: Sequence,Sequence,Literal: ')',Repeat: Sequence,Sequence,Literal: ',',Literal: '(',constant: Alternative,Alternative,STRING: Sequence,Sequence,Literal: '"',Repeat: Alternative,Alternative,Literal: '\',STRINGCHAR: Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-',Char: 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9',0x20,'.',',',';',':','!','?','+','*','/','=','<','>','(',')','_','^','%','&','|','~','@','#','$',''','-',Literal: '"',NUMBER: Sequence,Sequence,Optional: Sequence,Sequence,Repeat: Char: 0x30 to 0x39,Char: 0x30 to 0x39,Literal: '.',Repeat: Char: 0x30 to 0x39,Char: 0x30 to 0x39,Literal: 'false',Literal: 'true',Literal: '=',Optional: Literal: 'let',Literal: 'let',Optional: Literal: 'mutable',Literal: 'mutable',Optional: Literal: 'constant',Literal: 'constant' -Assert.That(result.Success, Is.True) - Expected: True - But was: False - - at Strict.Grammar.Tests.GrammarTests.CheckAllBaseFiles() in C:\code\GitHub\strict-lang\Strict\Strict.Grammar.Tests\GrammarTests.cs:line 50 - -1) at Strict.Grammar.Tests.GrammarTests.CheckAllBaseFiles() in C:\code\GitHub\strict-lang\Strict\Strict.Grammar.Tests\GrammarTests.cs:line 50 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NUnit Adapter 4.6.0.0: Test execution started -Running all tests in C:\code\GitHub\strict-lang\Strict\Strict.Grammar.Tests\bin\Debug\net10.0\Strict.Grammar.Tests.dll - NUnit3TestExecutor discovered 5 of 5 NUnit test cases using Current Discovery mode, Non-Explicit run -NUnit Adapter 4.6.0.0: Test execution complete - - - - \ No newline at end of file diff --git a/Strict.Grammar/Program.cs b/Strict.Grammar/Program.cs index 7b169b18..e01f28fc 100644 --- a/Strict.Grammar/Program.cs +++ b/Strict.Grammar/Program.cs @@ -7,7 +7,7 @@ EbnfStyle.EscapeTerminalStrings); var s = File.ReadAllText("Strict.ebnf"); var built = g.Build(s, "file"); -built.Separator = (Eto.Parse.Terminals.Set(' ') | Eto.Parse.Terminals.Set('\r')).Repeat(0); +built.Separator = (Terminals.Set(' ') | Terminals.Set('\r')).Repeat(0); ShowResult("has number", built.Match("has number")); ShowResult("dot notation", built.Match("Run\n\tvalue.Length\n")); ShowResult("dot simple", built.Match("value.Length")); @@ -21,18 +21,15 @@ ShowResult("group expr", built.Match("Run\n\tnot (true)\n")); ShowResult("string", built.Match("Run\n\t\"hello\"\n")); ShowResult("decimal", built.Match("has number\n")); -const string DefaultStrictBasePath = Repositories.StrictDevelopmentFolderPrefix + "Base"; -var basePath = Directory.Exists(DefaultStrictBasePath) - ? DefaultStrictBasePath - : Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "..", "..", "Strict.Base"); -foreach (var file in Directory.GetFiles(basePath, "*.strict")) +var localPath = Repositories.GetLocalDevelopmentPath(Repositories.StrictOrg, nameof(Strict)); +var basePath = Directory.Exists(localPath) + ? localPath + : Path.Combine(Directory.GetCurrentDirectory(), "..", "..", ".."); +foreach (var file in Directory.GetFiles(basePath, "*" + Strict.Language.Type.Extension)) ShowResult(Path.GetFileName(file), built.Match(File.ReadAllText(file))); static void ShowResult(string filename, GrammarMatch result) { Console.WriteLine( $"{filename}: Success={result.Success}, Length={result.Length}, Errors={result.Errors.Count()}, ErrorIndex={result.ErrorIndex}"); - //if (!result.Success) - // foreach (var error in result.Errors) - // Console.WriteLine(error.GetErrorMessage()); } \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/DictionaryTests.cs b/Strict.HighLevelRuntime.Tests/DictionaryTests.cs index f4791742..3c11deb5 100644 --- a/Strict.HighLevelRuntime.Tests/DictionaryTests.cs +++ b/Strict.HighLevelRuntime.Tests/DictionaryTests.cs @@ -8,7 +8,8 @@ namespace Strict.HighLevelRuntime.Tests; public sealed class DictionaryTests { [SetUp] - public void CreateExecutor() => executor = new Executor(TestBehavior.Disabled); + public void CreateExecutor() => + executor = new Executor(TestPackage.Instance, TestBehavior.Disabled); private Executor executor = null!; @@ -16,14 +17,25 @@ private static Type CreateType(string name, params string[] lines) => new Type(TestPackage.Instance, new TypeLines(name, lines)).ParseMembersAndMethods( new MethodExpressionParser()); + [Test] + public void DictionaryExpressionEvaluatesToEmptyDictionary() + { + using var t = CreateType(nameof(DictionaryExpressionEvaluatesToEmptyDictionary), "has number", + "Run Dictionary(Number, Number)", "\tDictionary(Number, Number)"); + var method = t.Methods.Single(m => m.Name == "Run"); + var result = executor.Execute(method); + Assert.That(result.IsDictionary, Is.True); + Assert.That(result.GetDictionaryItems().Count, Is.EqualTo(0)); + } + [Test] public void DictionaryTypeExpressionHasLengthZero() { using var t = CreateType(nameof(DictionaryTypeExpressionHasLengthZero), "has number", "Run Number", "\tDictionary(Number, Number).Length"); var method = t.Methods.Single(m => m.Name == "Run"); - var result = executor.Execute(method, null, []); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(0)); + var result = executor.Execute(method); + Assert.That(result.Number, Is.EqualTo(0)); } [Test] @@ -31,13 +43,12 @@ public void DictionaryAddReturnsDictionaryInstance() { using var t = CreateType(nameof(DictionaryAddReturnsDictionaryInstance), "has number", "Run Dictionary(Number, Number)", "\tDictionary((2, 4)).Add(4, 8)"); - var method = t.Methods.Single(m => m.Name == "Run"); - var result = executor.Execute(method, null, []); - Assert.That(result.ReturnType.Name, Is.EqualTo("Dictionary(Number, Number)")); - var values = (Dictionary)result.Value!; + var method = t.Methods.Single(m => m.Name == Method.Run); + var result = executor.Execute(method); + Assert.That(result.GetTypeExceptText().Name, Is.EqualTo("Dictionary(Number, Number)")); + var values = result.GetDictionaryItems(); Assert.That(values.Count, Is.EqualTo(2)); - Assert.That(values.Keys.Select(k => EqualsExtensions.NumberToDouble(k.Value)), - Does.Contain(2).And.Contain(4)); + Assert.That(values.Keys.Select(k => k.Number), Does.Contain(2).And.Contain(4)); } [Test] @@ -46,14 +57,11 @@ public void DictionaryValuesAreStoredInARealDictionary() using var t = CreateType(nameof(DictionaryValuesAreStoredInARealDictionary), "has number", "Run Dictionary(Number, Number)", "\tDictionary((1, 2)).Add(3, 4)"); var method = t.Methods.Single(m => m.Name == "Run"); - var result = executor.Execute(method, null, []); - var values = (Dictionary)result.Value!; - Assert.That(values.Keys.Select(k => EqualsExtensions.NumberToDouble(k.Value)), - Does.Contain(1).And.Contain(3)); - var key1 = values.Keys.First(key => EqualsExtensions.NumberToDouble(key.Value) == 1); - var key3 = values.Keys.First(key => EqualsExtensions.NumberToDouble(key.Value) == 3); - Assert.That(Convert.ToDouble(values[key1].Value), Is.EqualTo(2)); - Assert.That(Convert.ToDouble(values[key3].Value), Is.EqualTo(4)); + var result = executor.Execute(method); + var values = result.GetDictionaryItems(); + Assert.That(values.Keys.Select(k => k.Number), Does.Contain(1).And.Contain(3)); + Assert.That(values[values.Keys.First(key => key.Number == 1)].Number, Is.EqualTo(2)); + Assert.That(values[values.Keys.First(key => key.Number == 3)].Number, Is.EqualTo(4)); } [Test] @@ -62,8 +70,7 @@ public void DictionaryAddUsesKeysAndValuesPairs() using var t = CreateType(nameof(DictionaryAddUsesKeysAndValuesPairs), "has number", "Run Dictionary(Number, Number)", "\tDictionary((1, 2)).Add(3, 4).Add(5, 6)"); var method = t.Methods.Single(m => m.Name == "Run"); - var result = executor.Execute(method, null, []); - var values = (System.Collections.IDictionary)result.Value!; - Assert.That(values.Count, Is.EqualTo(3)); + var result = executor.Execute(method); + Assert.That(result.GetDictionaryItems().Count, Is.EqualTo(3)); } -} +} \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/ExecutionContextTests.cs b/Strict.HighLevelRuntime.Tests/ExecutionContextTests.cs index eb0dffaa..dd6b1e45 100644 --- a/Strict.HighLevelRuntime.Tests/ExecutionContextTests.cs +++ b/Strict.HighLevelRuntime.Tests/ExecutionContextTests.cs @@ -1,4 +1,4 @@ -using Strict.Language; +using Strict.Expressions; using Strict.Language.Tests; using Type = Strict.Language.Type; @@ -6,32 +6,30 @@ namespace Strict.HighLevelRuntime.Tests; public sealed class ExecutionContextTests { - [SetUp] - public void CreateType() => num = TestPackage.Instance.FindType(Base.Number)!; - - private Type num = null!; - [Test] public void SetAndGetVariable() { - var ctx = new ExecutionContext(num, num.Methods[0]); - var val = new ValueInstance(num, 123); + var numberType = TestPackage.Instance.GetType(Type.Number); + var ctx = new ExecutionContext(numberType, numberType.Methods[0]); + var val = new ValueInstance(numberType, 123); ctx.Set("answer", val); - Assert.That(ctx.Get("answer"), Is.SameAs(val)); + Assert.That(ctx.Get("answer", new Statistics()), Is.EqualTo(val)); } [Test] public void ParentLookupWorks() { - var parent = new ExecutionContext(num, num.Methods[0]); - var child = new ExecutionContext(num, num.Methods[0]) { Parent = parent }; - parent.Set("x", new ValueInstance(num, 5)); - Assert.That(child.Get("x").Value, Is.EqualTo(5)); + var numberType = TestPackage.Instance.GetType(Type.Number); + var parent = new ExecutionContext(numberType, numberType.Methods[0]); + var child = new ExecutionContext(numberType, numberType.Methods[0]) { Parent = parent }; + parent.Set("x", new ValueInstance(numberType, 5)); + Assert.That(child.Get("x", new Statistics()).Number, Is.EqualTo(5)); } [Test] public void GetUnknownVariableThrows() => Assert.That( - () => new ExecutionContext(num, num.Methods[0]).Get("unknown"), + () => new ExecutionContext(TestPackage.Instance.GetType(Type.Number), + TestPackage.Instance.GetType(Type.Number).Methods[0]).Get("unknown", new Statistics()), Throws.TypeOf()); } \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/ExecutorTests.cs b/Strict.HighLevelRuntime.Tests/ExecutorTests.cs index 55d89ccf..ac5d69f2 100644 --- a/Strict.HighLevelRuntime.Tests/ExecutorTests.cs +++ b/Strict.HighLevelRuntime.Tests/ExecutorTests.cs @@ -8,7 +8,8 @@ namespace Strict.HighLevelRuntime.Tests; public sealed class ExecutorTests { [SetUp] - public void CreateExecutor() => executor = new Executor(TestBehavior.Disabled); + public void CreateExecutor() => + executor = new Executor(TestPackage.Instance, TestBehavior.Disabled); private Executor executor = null!; @@ -17,7 +18,7 @@ public void MissingArgument() { using var t = CreateCalcType(); var method = t.Methods.Single(m => m.Name == "Add"); - Assert.That(() => executor.Execute(method, null, []), + Assert.That(() => executor.Execute(method), Throws.TypeOf().With.Message.StartsWith("first")); } @@ -27,7 +28,7 @@ public void FromConstructorWithExistingInstanceThrows() using var t = CreateType(nameof(FromConstructorWithExistingInstanceThrows), "has number", "from(number Number)", "\tvalue"); var method = t.Methods.Single(m => m.Name == Method.From); - var number = new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 3); + var number = new ValueInstance(executor.numberType, 3); var instance = new ValueInstance(t, 1); Assert.That(() => executor.Execute(method, instance, [number]), Throws.InstanceOf()); @@ -46,9 +47,9 @@ public void UseDefaultValue() { using var t = CreateCalcType(); var method = t.Methods.Single(m => m.Name == "Add"); - var result = executor.Execute(method, null, - [new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 5)]); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(6)); + var result = executor.Execute(method, executor.noneInstance, + [new ValueInstance(executor.numberType, 5)]); + Assert.That(result.Number, Is.EqualTo(6)); } [Test] @@ -56,11 +57,11 @@ public void TooManyArguments() { using var t = CreateCalcType(); var method = t.Methods.Single(m => m.Name == "Add"); - Assert.That(() => executor.Execute(method, null, [ - new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 1), - new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 2), - new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 3) - ]), Throws.InstanceOf().With.Message.StartsWith("Number:3")); + Assert.That(() => executor.Execute(method, executor.noneInstance, [ + new ValueInstance(executor.numberType, 1), + new ValueInstance(executor.numberType, 2), + new ValueInstance(executor.numberType, 3) + ]), Throws.InstanceOf().With.Message.StartsWith("Number: 3")); } [Test] @@ -69,8 +70,7 @@ public void ArgumentDoesNotMapToMethodParameters() using var t = CreateType(nameof(ArgumentDoesNotMapToMethodParameters), "has number", "Use(number Number) Number", "\tnumber"); var method = t.Methods.Single(m => m.Name == "Use"); - var boolean = new ValueInstance(TestPackage.Instance.FindType(Base.Boolean)!, true); - Assert.That(() => executor.Execute(method, null, [boolean]), + Assert.That(() => executor.Execute(method, executor.noneInstance, [executor.trueInstance]), Throws.InstanceOf()); } @@ -79,11 +79,10 @@ public void EvaluateValueAndVariableAndParameterCalls() { using var t = CreateCalcType(); var method = t.Methods.Single(m => m.Name == "Add"); - var first = new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 5); - var second = new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 7); - var result = executor.Execute(method, null, [first, second]); - Assert.That(result.ReturnType.Name, Is.EqualTo(Base.Number)); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(12)); + var first = new ValueInstance(executor.numberType, 5); + var second = new ValueInstance(executor.numberType, 7); + var result = executor.Execute(method, executor.noneInstance, [first, second]); + Assert.That(result.Number, Is.EqualTo(12)); } [Test] @@ -92,130 +91,106 @@ public void EvaluateDeclaration() using var t = CreateType(nameof(EvaluateDeclaration), "mutable last Number", "AddFive(number) Number", "\tconstant five = 5", "\tnumber + five"); var method = t.Methods.Single(m => m.Name == "AddFive"); - var number = new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 5); - var result = executor.Execute(method, null, [number]); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(10)); + var number = new ValueInstance(executor.numberType, 5); + var result = executor.Execute(method, executor.noneInstance, [number]); + Assert.That(result.Number, Is.EqualTo(10)); } [Test] public void EvaluateAllArithmeticOperators() { - using var t = CreateType(nameof(EvaluateAllArithmeticOperators), "mutable last Number", - "Plus(first Number, second Number) Number", "\tfirst + second", - "Minus(first Number, second Number) Number", "\tfirst - second", - "Mul(first Number, second Number) Number", "\tfirst * second", - "Div(first Number, second Number) Number", "\tfirst / second", - "Mod(first Number, second Number) Number", "\tfirst % second", - "Pow(first Number, second Number) Number", "\tfirst ^ second"); - static ValueInstance N(double x) => new(TestPackage.Instance.FindType(Base.Number)!, x); - Assert.That(Convert.ToDouble(executor.Execute(t.Methods.Single(m => m.Name == "Plus"), null, [ - N(2), N(3) - ]).Value), Is.EqualTo(5)); - Assert.That(Convert.ToDouble(executor.Execute(t.Methods.Single(m => m.Name == "Minus"), null, - [ - N(8), N(3) - ]).Value), Is.EqualTo(5)); - Assert.That(Convert.ToDouble(executor.Execute(t.Methods.Single(m => m.Name == "Mul"), null, [ - N(6), N(7) - ]).Value), Is.EqualTo(42)); - Assert.That(Convert.ToDouble(executor.Execute(t.Methods.Single(m => m.Name == "Div"), null, [ - N(8), N(2) - ]).Value), Is.EqualTo(4)); - Assert.That(Convert.ToDouble(executor.Execute(t.Methods.Single(m => m.Name == "Mod"), null, [ - N(8), N(3) - ]).Value), Is.EqualTo(2)); - Assert.That(Convert.ToDouble(executor.Execute(t.Methods.Single(m => m.Name == "Pow"), null, [ - N(2), N(3) - ]).Value), Is.EqualTo(8)); + var number = TestPackage.Instance.GetType(Type.Number); + + Method GetBinaryOperator(string op) => + number.Methods.Single(m => m.Name == op && m.Parameters.Count == 1); + + ValueInstance N(double x) => new(number, x); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Plus), N(2), [N(3)]).Number, + Is.EqualTo(5)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Minus), N(8), [N(3)]).Number, + Is.EqualTo(5)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Multiply), N(6), [N(7)]).Number, + Is.EqualTo(42)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Divide), N(8), [N(2)]).Number, + Is.EqualTo(4)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Modulate), N(8), [N(3)]).Number, + Is.EqualTo(2)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Power), N(2), [N(3)]).Number, + Is.EqualTo(8)); } [Test] public void AddTwoTexts() { - using var t = CreateType(nameof(AddTwoTexts), "has text", - "Concat(text Text, other Text) Text", "\ttext + other"); - var textType = t.GetType(Base.Text); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Concat"), null, - [new ValueInstance(textType, "hi "), new ValueInstance(textType, "there")]); - Assert.That(result.Value, Is.EqualTo("hi there")); + var text = TestPackage.Instance.GetType(Type.Text); + var plusText = text.Methods.Single(m => + m.Name == BinaryOperator.Plus && m.Parameters is [{ Type.IsText: true }]); + var result = executor.Execute(plusText, new ValueInstance("hi "), [new ValueInstance("there")]); + Assert.That(result.Text, Is.EqualTo("hi there")); } [Test] public void EvaluateAllComparisonOperators() { - using var t = CreateType(nameof(EvaluateAllComparisonOperators), "mutable last Number", - "Gt(first Number, second Number) Boolean", "\tfirst > second", - "Lt(first Number, second Number) Boolean", "\tfirst < second", - "Gte(first Number, second Number) Boolean", "\tfirst >= second", - "Lte(first Number, second Number) Boolean", "\tfirst <= second", - "Eq(first Number, second Number) Boolean", "\tfirst is second", - "Neq(first Number, second Number) Boolean", "\tfirst is not second"); - var num = TestPackage.Instance.FindType(Base.Number)!; - ValueInstance N(double x) => new(num, x); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Gt"), null, [N(5), N(3)]).Value, - Is.EqualTo(true)); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Lt"), null, [N(2), N(3)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Gte"), null, [N(5), N(3)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Gte"), null, [N(5), N(5)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Lte"), null, [N(2), N(3)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Lte"), null, [N(3), N(3)]).Value, - Is.EqualTo(true)); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Eq"), null, [N(3), N(3)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Neq"), null, [N(3), N(4)]).Value, - Is.EqualTo(true)); + var number = TestPackage.Instance.GetType(Type.Number); + + Method GetBinaryOperator(string op) => + number.Methods.Single(m => m.Name == op && m.Parameters.Count == 1); + + ValueInstance N(double x) => new(number, x); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Greater), N(5), [N(3)]), + Is.EqualTo(executor.trueInstance)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Smaller), N(2), [N(3)]), + Is.EqualTo(executor.trueInstance)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.GreaterOrEqual), N(5), [N(3)]), + Is.EqualTo(executor.trueInstance)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.GreaterOrEqual), N(5), [N(5)]), + Is.EqualTo(executor.trueInstance)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.SmallerOrEqual), N(2), [N(3)]), + Is.EqualTo(executor.trueInstance)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.SmallerOrEqual), N(3), [N(3)]), + Is.EqualTo(executor.trueInstance)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Is), N(3), [N(3)]), + Is.EqualTo(executor.trueInstance)); + Assert.That(executor.Execute(GetBinaryOperator(BinaryOperator.Is), N(3), [N(4)]), + Is.EqualTo(executor.falseInstance)); } [Test] public void EvaluateAllLogicalOperators() { - using var t = CreateType(nameof(EvaluateAllLogicalOperators), "has unused Boolean", - "And(first Boolean, second Boolean) Boolean", "\tfirst and second", - "Or(first Boolean, second Boolean) Boolean", "\tfirst or second", - "Xor(first Boolean, second Boolean) Boolean", "\tfirst xor second", - "Not(first Boolean) Boolean", "\tnot first"); - var boolType = TestPackage.Instance.FindType(Base.Boolean)!; - ValueInstance B(bool x) => new(boolType, x); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "And"), null, [B(true), B(true)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "And"), null, [B(true), B(false)]).Value, - Is.EqualTo(false)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Or"), null, [B(true), B(false)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Or"), null, [B(false), B(false)]).Value, - Is.EqualTo(false)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Xor"), null, [B(true), B(false)]).Value, - Is.EqualTo(true)); - Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Xor"), null, [B(true), B(true)]).Value, - Is.EqualTo(false)); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Not"), null, [B(false)]).Value, - Is.EqualTo(true)); + var boolean = TestPackage.Instance.GetType(Type.Boolean); + + Method GetBinaryOperator(string op) => + boolean.Methods.Single(m => + m.Name == op && m.ReturnType.IsBoolean && m.Parameters is [{ Type.IsBoolean: true }]); + + var not = boolean.Methods.Single(m => + m.Name == UnaryOperator.Not && m.ReturnType.IsBoolean && m.Parameters.Count == 0); + var and = GetBinaryOperator(BinaryOperator.And); + AssertBooleanOperation(and, true, true, true); + AssertBooleanOperation(and, true, false, false); + var or = GetBinaryOperator(BinaryOperator.Or); + AssertBooleanOperation(or, true, false, false); + AssertBooleanOperation(or, false, false, true); + var xor = GetBinaryOperator(BinaryOperator.Xor); + AssertBooleanOperation(xor, true, false, true); + AssertBooleanOperation(xor, true, true, false); + Assert.That(executor.Execute(not, executor.falseInstance, []), Is.EqualTo(executor.trueInstance)); } + private void AssertBooleanOperation(Method method, bool first, bool second, bool result) => + Assert.That(executor.Execute(method, executor.ToBoolean(first), [executor.ToBoolean(second)]), + Is.EqualTo(executor.ToBoolean(result))); + [Test] public void EvaluateMemberCallFromStaticConstant() { using var t = CreateType(nameof(EvaluateMemberCallFromStaticConstant), "mutable last Number", "GetTab Character", "\tCharacter.Tab"); - var method = t.Methods.Single(m => m.Name == "GetTab"); - var result = executor.Execute(method, null, []); - Assert.That(result.ReturnType.Name, Is.EqualTo(Base.Character)); - Assert.That(result.Value, Is.EqualTo(7)); + var result = executor.Execute(t.Methods.Single(m => m.Name == "GetTab")); + Assert.That(result.IsPrimitiveType(executor.characterType), Is.True, result.ToString()); + Assert.That(result.Number, Is.EqualTo(7)); } [Test] @@ -223,10 +198,11 @@ public void EvaluateBooleanComparisons() { using var t = CreateType(nameof(EvaluateBooleanComparisons), "mutable last Boolean", "IfDifferent Boolean", "\tlast is false"); - var method = t.Methods.Single(m => m.Name == "IfDifferent"); - var result = executor.Execute(method, - new ValueInstance(t, new Dictionary { { "last", false } }), []); - Assert.That(Convert.ToBoolean(result.Value), Is.EqualTo(true)); + Assert.That( + executor.Execute(t.Methods.Single(m => m.Name == "IfDifferent"), + new ValueInstance(t, + new Dictionary { { "last", executor.falseInstance } }), []), + Is.EqualTo(executor.trueInstance)); } [Test] @@ -234,8 +210,8 @@ public void EvaluateRangeEquality() { using var t = CreateType(nameof(EvaluateRangeEquality), "has number", "Compare Boolean", "\tRange(0, 5) is Range(0, 5)"); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Compare"), null, []); - Assert.That(result.Value, Is.EqualTo(true)); + Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Compare"), executor.noneInstance, + []), Is.EqualTo(executor.trueInstance)); } [Test] @@ -243,8 +219,12 @@ public void MultilineMethodRequiresTests() { using var t = CreateType(nameof(MultilineMethodRequiresTests), "has number", "GetText Text", "\tif number is 0", "\t\treturn \"\"", "\tnumber to Text"); - var instance = new ValueInstance(t, 5); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "GetText"), instance, []).Value, + var instance = new ValueInstance(t, + new Dictionary + { + { "number", new ValueInstance(executor.numberType, 5.0) } + }); + Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "GetText"), instance, []).Text, Is.EqualTo("5")); } @@ -254,8 +234,8 @@ public void MethodWithoutTestsThrowsMethodRequiresTestDuringValidation() using var t = CreateType("NoTestsNeedValidation", "has number", "Compute Number", "\tif true", "\t\treturn 1", "\t2"); var method = t.Methods.Single(m => m.Name == "Compute"); - var validatingExecutor = new Executor(); - var ex = Assert.Throws(() => validatingExecutor.Execute(method, null, [])); + var validatingExecutor = new Executor(TestPackage.Instance); + var ex = Assert.Throws(() => validatingExecutor.Execute(method)); Assert.That(ex!.InnerException, Is.InstanceOf()); } @@ -265,8 +245,8 @@ public void MethodRequiresTestWhenParsingFailsDuringValidation() using var t = CreateType(nameof(MethodRequiresTestWhenParsingFailsDuringValidation), "has number", "Compute Number", "\tunknown(1)"); var method = t.Methods.Single(m => m.Name == "Compute"); - var validatingExecutor = new Executor(); - Assert.That(() => validatingExecutor.Execute(method, null, []), + var validatingExecutor = new Executor(TestPackage.Instance); + Assert.That(() => validatingExecutor.Execute(method), Throws.InstanceOf()); } @@ -276,9 +256,9 @@ public void InvalidTypeForFromConstructor() using var t = CreateType(nameof(InvalidTypeForFromConstructor), "has flag Boolean", "from(flag Boolean, other Boolean)", "\tvalue"); var method = t.Methods.Single(m => m.Name == Method.From); - var number = new ValueInstance(TestPackage.Instance.FindType(Base.Number)!, 1); - var boolean = new ValueInstance(TestPackage.Instance.FindType(Base.Boolean)!, true); - Assert.That(() => executor.Execute(method, null, [number, boolean]), + var number = new ValueInstance(executor.numberType, 1.0); + var boolean = executor.trueInstance; + Assert.That(() => executor.Execute(method, executor.noneInstance, [number, boolean]), Throws.InstanceOf()); } @@ -288,11 +268,11 @@ public void FromConstructorConvertsSingleCharText() using var t = CreateType(nameof(FromConstructorConvertsSingleCharText), "has number", "has text", "from(number Number, text Text)", "\tvalue"); var method = t.Methods.Single(m => m.Name == Method.From); - var numberText = new ValueInstance(TestPackage.Instance.FindType(Base.Text)!, "A"); - var text = new ValueInstance(TestPackage.Instance.FindType(Base.Text)!, "ok"); - var result = executor.Execute(method, null, [numberText, text]); - var values = (IDictionary)result.Value!; - Assert.That(values["number"], Is.EqualTo(65)); + var numberText = new ValueInstance("A"); + var text = new ValueInstance("ok"); + var result = executor.Execute(method, executor.noneInstance, [numberText, text]); + var members = result.TryGetValueTypeInstance()!.Members; + Assert.That(members["number"].Number, Is.EqualTo(65)); } [Test] @@ -300,8 +280,9 @@ public void CompareNumberToText() { using var t = CreateType(nameof(CompareNumberToText), "has number", "Compare", "\t\"5\" is 5"); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Compare"), null, []).Value, - Is.EqualTo(false)); + Assert.That( + executor.Execute(t.Methods.Single(m => m.Name == "Compare"), executor.noneInstance, []), + Is.EqualTo(executor.trueInstance)); } [Test] @@ -309,8 +290,9 @@ public void CompareTextToCharacterTab() { using var t = CreateType(nameof(CompareTextToCharacterTab), "has number", "Compare Boolean", "\t\"7\" is Character.Tab"); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Compare"), null, []).Value, - Is.EqualTo(true)); + Assert.That( + executor.Execute(t.Methods.Single(m => m.Name == "Compare"), executor.noneInstance, []), + Is.EqualTo(executor.falseInstance)); } [Test] @@ -327,9 +309,11 @@ public void StackOverflowCallingYourselfWithSameInstanceMember() { using var t = CreateType(nameof(StackOverflowCallingYourselfWithSameInstanceMember), "has number", "Recursive(other Number)", "\tRecursive(number)"); - Assert.That( - () => executor.Execute(t.Methods.Single(m => m.Name == "Recursive"), - new ValueInstance(t, 3), [new ValueInstance(t.GetType(Base.Number), 1)]).Value, + Assert.That(() => executor.Execute(t.Methods.Single(m => m.Name == "Recursive"), + new ValueInstance(t, new Dictionary + { + { "number", new ValueInstance(executor.numberType, 3.0) } + }), [new ValueInstance(executor.numberType, 1.0)]), Throws.InstanceOf()); } @@ -338,8 +322,106 @@ public void CallNumberPlusOperator() { using var t = CreateType(nameof(CallNumberPlusOperator), "has number", "+(text) Number", "\tnumber + text.Length"); + Assert.That(executor.Execute(t.Methods.Single(m => m.Name == BinaryOperator.Plus), + new ValueInstance(t, new Dictionary + { + { "number", new ValueInstance(executor.numberType, 5.0) } + }), + [new ValueInstance("abc")]).Number, Is.EqualTo(5 + 3)); + } + + [Test] + public void InlineTestSkipListDeclarationReferencingMember() + { + using var t = CreateType(nameof(InlineTestSkipListDeclarationReferencingMember), + "has first Number", "has second Number", "GetCount Number", + "\t(1, 2, 3).Length is 3", + "\tlet myList = (second, 2, 3)", + "\tmyList.Length"); + var validatingExecutor = new Executor(TestPackage.Instance); + Assert.That( + validatingExecutor.Execute(t.Methods.Single(m => m.Name == "GetCount"), + executor.noneInstance, [], null, true).Number, Is.EqualTo(3)); + } + + [Test] + public void InlineDictionaryDeclarationLength() + { + using var t = CreateType(nameof(InlineDictionaryDeclarationLength), + "has number", "GetCount Number", + "\t(1, 2, 3).Length is 3", + "\tconstant myDict = Dictionary(Number, Number)", + "\tmyDict.Length"); + var validatingExecutor = new Executor(TestPackage.Instance); + Assert.That( + validatingExecutor.Execute(t.Methods.Single(m => m.Name == "GetCount")).Number, Is.EqualTo(0)); + } + + [Test] + public void InlineTestDictionaryDeclaration() + { + using var t = CreateType(nameof(InlineTestDictionaryDeclaration), + "has number", "Run Number", + "\tconstant myDict = Dictionary(Text, Text)", + "\tmyDict.Length is 0", + "\tnumber"); + var validatingExecutor = new Executor(TestPackage.Instance); + Assert.That( + validatingExecutor.Execute(t.Methods.Single(m => m.Name == "Run")).Number, Is.EqualTo(0)); + } + + [Test] + public void CannotCallMethodWithWrongInstanceThrows() + { + using var t = CreateType(nameof(CannotCallMethodWithWrongInstanceThrows), "has number", + "Compute Number", "\tnumber"); + var method = t.Methods.Single(m => m.Name == "Compute"); + Assert.That(() => executor.Execute(method, executor.trueInstance, []), + Throws.InstanceOf()); + } + + [Test] + public void MutableDeclarationWithMutableValueTracksStatistics() + { + using var t = CreateType(nameof(MutableDeclarationWithMutableValueTracksStatistics), + "has number", "Run Number", + "\tmutable vx = number", + "\tmutable vy = vx", + "\tvx + vy"); + executor.Execute(t.Methods.Single(m => m.Name == "Run"), executor.noneInstance, []); + Assert.That(executor.Statistics.MutableDeclarationCount, Is.EqualTo(1)); + Assert.That(executor.Statistics.MutableUsageCount, Is.EqualTo(1)); + } + + [Test] + public void IsNotOperatorReturnsTrueForDifferentValues() + { + using var t = CreateType(nameof(IsNotOperatorReturnsTrueForDifferentValues), "has number", + "Check Boolean", "\t1 is not 2"); + Assert.That( + executor.Execute(t.Methods.Single(m => m.Name == "Check"), executor.noneInstance, []), + Is.EqualTo(executor.trueInstance)); + } + + [Test] + public void IsNotOperatorReturnsFalseForSameValues() + { + using var t = CreateType(nameof(IsNotOperatorReturnsFalseForSameValues), "has number", + "Check Boolean", "\t1 is not 1"); + Assert.That( + executor.Execute(t.Methods.Single(m => m.Name == "Check"), executor.noneInstance, []), + Is.EqualTo(executor.falseInstance)); + } + + [Test] + public void IsNotErrorReturnsFalseWhenBothAreErrors() + { + using var t = CreateType(nameof(IsNotErrorReturnsFalseWhenBothAreErrors), "has number", + "Check Boolean", + "\tconstant err = Error(\"test\")", + "\terr is not err"); Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == BinaryOperator.Plus), new ValueInstance(t, 5), - [new ValueInstance(t.GetType(Base.Text), "abc")]).Value, Is.EqualTo(5 + 3)); + executor.Execute(t.Methods.Single(m => m.Name == "Check"), executor.noneInstance, []), + Is.EqualTo(executor.falseInstance)); } } \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/ForEvaluatorTests.cs b/Strict.HighLevelRuntime.Tests/ForEvaluatorTests.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/Strict.HighLevelRuntime.Tests/ForTests.cs b/Strict.HighLevelRuntime.Tests/ForTests.cs index f4fa163f..d850e475 100644 --- a/Strict.HighLevelRuntime.Tests/ForTests.cs +++ b/Strict.HighLevelRuntime.Tests/ForTests.cs @@ -8,7 +8,7 @@ namespace Strict.HighLevelRuntime.Tests; public sealed class ForTests { [SetUp] - public void CreateExecutor() => executor = new Executor(TestBehavior.Disabled); + public void CreateExecutor() => executor = new Executor(TestPackage.Instance, TestBehavior.Disabled); private Executor executor = null!; @@ -21,8 +21,8 @@ public void CustomVariableInForLoopIsUsed() { using var t = CreateType(nameof(CustomVariableInForLoopIsUsed), "has number", "Sum Number", "\tfor item in (1, 2, 3)", "\t\titem"); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Sum"), null, []); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(6)); + var result = executor.Execute(t.Methods.Single(m => m.Name == "Sum"), executor.noneInstance, []); + Assert.That(result.Number, Is.EqualTo(6)); } [TestCase("add", 6)] @@ -33,9 +33,12 @@ public void SelectorIfInForUsesOperation(string operation, double expected) "Run Number", "\tfor (1, 2, 3)", "\t\tif operation is", "\t\t\t\"add\" then value", "\t\t\t\"subtract\" then 0 - value"); var instance = new ValueInstance(t, - new Dictionary { { "operation", operation } }); + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "operation", new ValueInstance(operation) } + }); var result = executor.Execute(t.Methods.Single(m => m.Name == "Run"), instance, []); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(expected)); + Assert.That(result.Number, Is.EqualTo(expected)); } [Test] @@ -43,8 +46,8 @@ public void TextReturnTypeConsolidatesNumbers() { using var t = CreateType(nameof(TextReturnTypeConsolidatesNumbers), "has number", "Join Text", "\tfor (\"a\", \"abc\")", "\t\tvalue.Length"); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Join"), null, []); - Assert.That(result.Value, Is.EqualTo("13")); + var result = executor.Execute(t.Methods.Single(m => m.Name == "Join"), executor.noneInstance, []); + Assert.That(result.Text, Is.EqualTo("13")); } [Test] @@ -52,8 +55,8 @@ public void TextReturnTypeConsolidatesTexts() { using var t = CreateType(nameof(TextReturnTypeConsolidatesTexts), "has number", "Join Text", "\tfor (\"hello \", \"world\")", "\t\tvalue"); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Join"), null, []); - Assert.That(result.Value, Is.EqualTo("hello world")); + var result = executor.Execute(t.Methods.Single(m => m.Name == "Join"), executor.noneInstance, []); + Assert.That(result.Text, Is.EqualTo("hello world")); } [Test] @@ -61,9 +64,9 @@ public void TextReturnTypeConsolidatesCharacters() { using var t = CreateType(nameof(TextReturnTypeConsolidatesCharacters), "has number", "Join(character) Text", "\tfor (character, character)", "\t\tvalue"); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Join"), null, - [new ValueInstance(t.GetType(Base.Character), 'b')]); - Assert.That(result.Value, Is.EqualTo("bb")); + var result = executor.Execute(t.Methods.Single(m => m.Name == "Join"), executor.noneInstance, + [new ValueInstance(t.GetType(Type.Character), 'b')]); + Assert.That(result.Text, Is.EqualTo("bb")); } [Test] @@ -71,7 +74,7 @@ public void TextReturnTypeThrowsWhenUnsupportedValueIsUsed() { using var t = CreateType(nameof(TextReturnTypeThrowsWhenUnsupportedValueIsUsed), "has number", "Join Text", "\tfor (1, 2)", "\t\tError(\"boom\")"); - Assert.That(() => executor.Execute(t.Methods.Single(m => m.Name == "Join"), null, []), + Assert.That(() => executor.Execute(t.Methods.Single(m => m.Name == "Join"), executor.noneInstance, []), Throws.InstanceOf()); } @@ -80,8 +83,8 @@ public void ForLoopUsesNumberIteratorLength() { using var t = CreateType(nameof(ForLoopUsesNumberIteratorLength), "has number", "Run Number", "\tfor 3", "\t\t1"); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Run"), null, []); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(3)); + var result = executor.Execute(t.Methods.Single(m => m.Name == "Run"), executor.noneInstance, []); + Assert.That(result.Number, Is.EqualTo(3)); } [Test] @@ -89,8 +92,8 @@ public void ForLoopUsesFloatingNumberIteratorLength() { using var t = CreateType(nameof(ForLoopUsesFloatingNumberIteratorLength), "has number", "Run Number", "\tfor 2.5", "\t\t1"); - var result = executor.Execute(t.Methods.Single(m => m.Name == "Run"), null, []); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(2)); + var result = executor.Execute(t.Methods.Single(m => m.Name == "Run"), executor.noneInstance, []); + Assert.That(result.Number, Is.EqualTo(2)); } [Test] @@ -100,8 +103,11 @@ public void ForLoopThrowsWhenIteratorLengthIsUnsupported() using var t = CreateType(TypeName, "has numbers", $"Run(container {TypeName}) Number", "\tfor container", "\t\t1"); var container = new ValueInstance(t, - new Dictionary { { "numbers", new List() } }); - Assert.That(() => executor.Execute(t.Methods.Single(m => m.Name == "Run"), null, [container]), + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "numbers", new ValueInstance(t.Members[0].Type, new List()) } + }); + Assert.That(() => executor.Execute(t.Methods.Single(m => m.Name == "Run"), executor.noneInstance, [container]), Throws.InstanceOf()); } @@ -111,14 +117,67 @@ public void GetElementsTextWithCompactConditionalThen() using var t = CreateType(nameof(GetElementsTextWithCompactConditionalThen), "has number", "GetElementsText(elements Numbers) Text", "\tfor elements", "\t\t(index is 0 then \"\" else \", \") + value"); - var numberType = TestPackage.Instance.FindType(Base.Number)!; - var result = executor.Execute(t.Methods.Single(m => m.Name == "GetElementsText"), null, - [ - new ValueInstance( - TestPackage.Instance.FindType(Base.List)!.GetGenericImplementation(numberType), - new List { new(numberType, 1.0), new(numberType, 3.0) }) - ]); - Assert.That(result.Value, Is.EqualTo("1, 3")); + var nums = new List + { + new(executor.numberType, 1.0), + new(executor.numberType, 3.0) + }; + var listType = executor.listType.GetGenericImplementation(executor.numberType); + var result = executor.Execute(t.Methods.Single(m => m.Name == "GetElementsText"), executor.noneInstance, + [new ValueInstance(listType, nums)]); + Assert.That(result.Text, Is.EqualTo("1, 3")); + } + + [Test] + public void ForLoopWithAscendingRangeAndEarlyReturn() + { + using var t = CreateType(nameof(ForLoopWithAscendingRangeAndEarlyReturn), "has number", + "FindFirst Number", + "\tfor Range(1, 5)", + "\t\tif value is 3", + "\t\t\treturn value", + "\t\t0"); + var result = executor.Execute(t.Methods.Single(m => m.Name == "FindFirst"), + executor.noneInstance, []); + Assert.That(result.Number, Is.EqualTo(3)); + } + + [Test] + public void ForLoopWithDescendingRangeAndEarlyReturn() + { + using var t = CreateType(nameof(ForLoopWithDescendingRangeAndEarlyReturn), "has number", + "FindFirst Number", + "\tfor Range(5, 1)", + "\t\tif value is 3", + "\t\t\treturn value", + "\t\t0"); + var result = executor.Execute(t.Methods.Single(m => m.Name == "FindFirst"), + executor.noneInstance, []); + Assert.That(result.Number, Is.EqualTo(3)); + } + + [Test] + public void TextReturnTypeWithNoResultsReturnsEmpty() + { + using var t = CreateType(nameof(TextReturnTypeWithNoResultsReturnsEmpty), "has number", + "GetText Text", + "\tmutable sum = 0", + "\tfor (1, 2, 3)", + "\t\tif value > 0", + "\t\t\tsum = sum + value"); + var result = executor.Execute(t.Methods.Single(m => m.Name == "GetText"), + executor.noneInstance, []); + Assert.That(result.Text, Is.EqualTo("")); + } + + [Test] + public void TextReturnTypeConsolidatesListValues() + { + using var t = CreateType(nameof(TextReturnTypeConsolidatesListValues), "has number", + "Merge Text", "\tfor (\"a\", \"b\")", "\t\t(value, value)"); + var result = executor.Execute(t.Methods.Single(m => m.Name == "Merge"), + executor.noneInstance, []); + Assert.That(result.Text, Is.EqualTo("(a, a), (b, b)")); } [Test] @@ -137,8 +196,8 @@ public void RemoveParenthesesWithElseIfChain() "\t\telse if parentheses is 0", "\t\t\tvalue"); var result = executor.Execute(t.Methods.Single(m => m.Name == "Remove"), - new ValueInstance(t, - new Dictionary { { "text", "example(unwanted)example" } }), []); - Assert.That(result.Value, Is.EqualTo("exampleexample")); + new ValueInstance(t, new Dictionary(StringComparer.OrdinalIgnoreCase) + { { "text", new ValueInstance("example(unwanted)example") } }), []); + Assert.That(result.Text, Is.EqualTo("exampleexample")); } } \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/IfTests.cs b/Strict.HighLevelRuntime.Tests/IfTests.cs index 86582668..33510fbe 100644 --- a/Strict.HighLevelRuntime.Tests/IfTests.cs +++ b/Strict.HighLevelRuntime.Tests/IfTests.cs @@ -8,7 +8,7 @@ namespace Strict.HighLevelRuntime.Tests; public sealed class IfTests { [SetUp] - public void CreateExecutor() => executor = new Executor(TestBehavior.Disabled); + public void CreateExecutor() => executor = new Executor(TestPackage.Instance, TestBehavior.Disabled); private Executor executor = null!; @@ -22,8 +22,8 @@ public void EvaluateIfTrueThenReturn() using var t = CreateType(nameof(EvaluateIfTrueThenReturn), "mutable last Number", "IfTrue Number", "\tif true", "\t\treturn 33", "\t0"); var method = t.Methods.Single(m => m.Name == "IfTrue"); - var result = executor.Execute(method, null, []); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(33)); + var result = executor.Execute(method, executor.noneInstance, []); + Assert.That(result.Number, Is.EqualTo(33)); } [Test] @@ -32,8 +32,8 @@ public void EvaluateIfFalseFallsThrough() using var t = CreateType(nameof(EvaluateIfFalseFallsThrough), "mutable last Number", "IfFalse Number", "\tif false", "\t\treturn 99", "\t42"); var method = t.Methods.Single(m => m.Name == "IfFalse"); - var result = executor.Execute(method, null, []); - Assert.That(Convert.ToDouble(result.Value), Is.EqualTo(42)); + var result = executor.Execute(method, executor.noneInstance, []); + Assert.That(result.Number, Is.EqualTo(42)); } [Test] @@ -41,15 +41,18 @@ public void EvaluateIsInEnumerableRange() { using var t = CreateType(nameof(EvaluateIsInEnumerableRange), "has number", "IsInRange(range Range) Boolean", "\tnumber is in range"); - var rangeType = TestPackage.Instance.FindType(Base.Range)!; - var rangeInstance = new ValueInstance(rangeType, - new Dictionary { { "Start", 1.0 }, { "ExclusiveEnd", 10.0 } }); + var rangeInstance = new ValueInstance(executor.rangeType, + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Start", new ValueInstance(executor.numberType, 1.0) }, + { "ExclusiveEnd", new ValueInstance(executor.numberType, 10.0) } + }); var result = executor.Execute(t.Methods.Single(m => m.Name == "IsInRange"), new ValueInstance(t, 7.0), [rangeInstance]); - Assert.That(result.Value, Is.EqualTo(true)); + Assert.That(result.Boolean, Is.EqualTo(true)); result = executor.Execute(t.Methods.Single(m => m.Name == "IsInRange"), new ValueInstance(t, 11.0), [rangeInstance]); - Assert.That(result.Value, Is.EqualTo(false)); + Assert.That(result.Boolean, Is.EqualTo(false)); } [Test] @@ -57,15 +60,18 @@ public void EvaluateIsNotInEnumerableRange() { using var t = CreateType(nameof(EvaluateIsNotInEnumerableRange), "has number", "IsNotInRange(range Range) Boolean", "\tnumber is not in range"); - var rangeType = TestPackage.Instance.FindType(Base.Range)!; - var rangeInstance = new ValueInstance(rangeType, - new Dictionary { { "Start", 1.0 }, { "ExclusiveEnd", 10.0 } }); + var rangeInstance = new ValueInstance(executor.rangeType, + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Start", new ValueInstance(executor.numberType, 1.0) }, + { "ExclusiveEnd", new ValueInstance(executor.numberType, 10.0) } + }); var result = executor.Execute(t.Methods.Single(m => m.Name == "IsNotInRange"), new ValueInstance(t, 11), [rangeInstance]); - Assert.That(result.Value, Is.EqualTo(true)); + Assert.That(result.Boolean, Is.EqualTo(true)); result = executor.Execute(t.Methods.Single(m => m.Name == "IsNotInRange"), new ValueInstance(t, 7), [rangeInstance]); - Assert.That(result.Value, Is.EqualTo(false)); + Assert.That(result.Boolean, Is.EqualTo(false)); } [Test] @@ -75,8 +81,8 @@ public void ReturnTypeMustMatchMethod() "Run(condition Boolean) Number", "\t5 is 5", "\tif condition", "\t\t6"); var method = t.Methods.Single(m => m.Name == "Run"); Assert.That( - () => executor.Execute(method, null, - [new ValueInstance(TestPackage.Instance.FindType(Base.Boolean)!, false)]), + () => executor.Execute(method, executor.noneInstance, + [new ValueInstance(TestPackage.Instance.FindType(Type.Boolean)!, false)]), Throws.TypeOf().With.InnerException. TypeOf()); } @@ -88,6 +94,6 @@ public void SelectorIfUsesElseWhenNoCaseMatches() "Run Boolean", "\tif value is", "\t\t2 then true", "\t\telse false"); var instance = new ValueInstance(t, 3.0); var result = executor.Execute(t.Methods.Single(m => m.Name == "Run"), instance, []); - Assert.That(result.Value, Is.EqualTo(false)); + Assert.That(result.Boolean, Is.EqualTo(false)); } -} +} \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/ListTests.cs b/Strict.HighLevelRuntime.Tests/ListTests.cs index 1aefb196..3afa77d5 100644 --- a/Strict.HighLevelRuntime.Tests/ListTests.cs +++ b/Strict.HighLevelRuntime.Tests/ListTests.cs @@ -10,21 +10,27 @@ public sealed class ListTests [SetUp] public void CreateExecutor() { - executor = new Executor(TestBehavior.Disabled); - one = new ValueInstance(TestPackage.Instance.GetType(Base.Number), 1); - two = new ValueInstance(TestPackage.Instance.GetType(Base.Number), 2); + executor = new Executor(TestPackage.Instance, TestBehavior.Disabled); + one = new ValueInstance(executor.numberType, 1); + two = new ValueInstance(executor.numberType, 2); } private Executor executor = null!; - private ValueInstance one = null!; - private ValueInstance two = null!; + private ValueInstance one; + private ValueInstance two; private static Type CreateType(string name, params string[] lines) => new Type(TestPackage.Instance, new TypeLines(name, lines)).ParseMembersAndMethods( new MethodExpressionParser()); private ValueInstance CreateNumbers(Type t) => - new(t, new Dictionary { { "numbers", new[] { one, two } } }); + new(t, + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { + "numbers", new ValueInstance(t.Members[0].Type, new List { one, two }) + } + }); [Test] public void CallListOperator() @@ -32,7 +38,7 @@ public void CallListOperator() using var t = CreateType(nameof(CallListOperator), "has numbers", "Double Numbers", "\tnumbers + numbers"); Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Double"), CreateNumbers(t), []).Value, + executor.Execute(t.Methods.Single(m => m.Name == "Double"), CreateNumbers(t), []).List.Items, Is.EqualTo(new[] { one, two, one, two })); } @@ -42,8 +48,8 @@ public void AddNumberToList() using var t = CreateType(nameof(AddNumberToList), "has numbers", "AddOne Numbers", "\tnumbers + 1"); Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "AddOne"), CreateNumbers(t), []).Value, - Is.EqualTo(new List { one, two, one })); + executor.Execute(t.Methods.Single(m => m.Name == "AddOne"), CreateNumbers(t), []).List.Items, + Is.EqualTo(new List { one, two, one })); } [Test] @@ -52,7 +58,7 @@ public void RemoveNumberFromList() using var t = CreateType(nameof(AddNumberToList), "has numbers", "RemoveOne Numbers", "\tnumbers - 1"); Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "RemoveOne"), CreateNumbers(t), []).Value, + executor.Execute(t.Methods.Single(m => m.Name == "RemoveOne"), CreateNumbers(t), []).List.Items, Is.EqualTo(new[] { two })); } @@ -62,8 +68,8 @@ public void MultiplyList() using var t = CreateType(nameof(MultiplyList), "has numbers", "Multiply Numbers", "\tnumbers * 2"); Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Multiply"), CreateNumbers(t), []).Value, - Is.EqualTo(new[] { two, new ValueInstance(t.GetType(Base.Number), 4) })); + executor.Execute(t.Methods.Single(m => m.Name == "Multiply"), CreateNumbers(t), []).List.Items, + Is.EqualTo(new[] { two, new ValueInstance(t.GetType(Type.Number), 4d) })); } [Test] @@ -72,11 +78,11 @@ public void DivideList() using var t = CreateType(nameof(DivideList), "has numbers", "Divide Numbers", "\tnumbers / 10"); Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Divide"), CreateNumbers(t), []).Value, + executor.Execute(t.Methods.Single(m => m.Name == "Divide"), CreateNumbers(t), []).List.Items, Is.EqualTo(new[] { - new ValueInstance(t.GetType(Base.Number), 0.1), - new ValueInstance(t.GetType(Base.Number), 0.2) + new ValueInstance(t.GetType(Type.Number), 0.1), + new ValueInstance(t.GetType(Type.Number), 0.2) })); } @@ -86,11 +92,11 @@ public void DivideLists() using var t = CreateType(nameof(DivideLists), "has numbers", "Divide Numbers", "\tnumbers / numbers"); Assert.That( - executor.Execute(t.Methods.Single(m => m.Name == "Divide"), CreateNumbers(t), []).Value, + executor.Execute(t.Methods.Single(m => m.Name == "Divide"), CreateNumbers(t), []).List.Items, Is.EqualTo(new[] { - new ValueInstance(t.GetType(Base.Number), 1.0), - new ValueInstance(t.GetType(Base.Number), 1.0) + new ValueInstance(t.GetType(Type.Number), 1.0), + new ValueInstance(t.GetType(Type.Number), 1.0) })); } @@ -100,9 +106,9 @@ public void ListsHaveDifferentDimensionsIsNotAllowed(string input) { using var t = CreateType(nameof(ListsHaveDifferentDimensionsIsNotAllowed), "has number", "Run", "\t" + input); - var error = executor.Execute(t.Methods[0], null, []); - Assert.That(error.ReturnType.Name, Is.EqualTo(Base.Error)); - Assert.That(error.FindInnerValue(Base.Name), + var error = executor.Execute(t.Methods[0], executor.noneInstance, []); + Assert.That(error.GetTypeExceptText().Name, Is.EqualTo(Type.Error)); + Assert.That(error.TryGetValueTypeInstance()!.Members["name"].Text, Is.EqualTo(MethodCallEvaluator.ListsHaveDifferentDimensions)); } @@ -113,7 +119,7 @@ public void RunListIn() new TypeLines(nameof(RunListIn), "has number", "Run Boolean", "\t\"d\" is not in (\"a\", \"b\", \"c\")", "\t\"b\" is in (\"a\", \"b\", \"c\")")). ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That(executor.Execute(type.Methods[0], null, []).Value, Is.EqualTo(true)); + Assert.That(executor.Execute(type.Methods[0], executor.noneInstance, []).Boolean, Is.EqualTo(true)); } [Test] @@ -123,7 +129,7 @@ public void RunListCount() new TypeLines(nameof(RunListCount), "has number", "GetCount Number", "\t(1, 2).Count(1)")). ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That(executor.Execute(type.Methods[0], null, []).Value, Is.EqualTo(1)); + Assert.That(executor.Execute(type.Methods[0], executor.noneInstance, []).Number, Is.EqualTo(1)); } [Test] @@ -132,10 +138,11 @@ public void RunListReverse() using var type = new Type(TestPackage.Instance, new TypeLines(nameof(RunListCount), "has number", "GetReverse List(Number)", "\t(1, 2).Reverse")).ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That(executor.Execute(type.Methods[0], null, []).Value, + Assert.That(executor.Execute(type.Methods[0], executor.noneInstance, []).List.Items, Is.EqualTo(new List { - new(type.GetType(Base.Number), 2.0), new(type.GetType(Base.Number), 1.0) + new(type.GetType(Type.Number), 2.0), + new(type.GetType(Type.Number), 1.0) })); } @@ -149,6 +156,6 @@ public void ListExpressionIsBecomesListOfValueInstances() new TypeLines(nameof(ListExpressionIsBecomesListOfValueInstances), "has number", "CompareLists", "\t(1, 2).Reverse is (2, 1)")). ParseMembersAndMethods(new MethodExpressionParser()); - executor.Execute(type.Methods[0], null, []); + executor.Execute(type.Methods[0], executor.noneInstance, []); } } \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/Strict.HighLevelRuntime.Tests.csproj b/Strict.HighLevelRuntime.Tests/Strict.HighLevelRuntime.Tests.csproj index 1fd6026c..8f1be199 100644 --- a/Strict.HighLevelRuntime.Tests/Strict.HighLevelRuntime.Tests.csproj +++ b/Strict.HighLevelRuntime.Tests/Strict.HighLevelRuntime.Tests.csproj @@ -9,10 +9,7 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + diff --git a/Strict.HighLevelRuntime.Tests/TestResults/RemoveParenthesesWithElseIfChain.trx b/Strict.HighLevelRuntime.Tests/TestResults/RemoveParenthesesWithElseIfChain.trx deleted file mode 100644 index e7255c03..00000000 --- a/Strict.HighLevelRuntime.Tests/TestResults/RemoveParenthesesWithElseIfChain.trx +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - Strict.Language.Body+ValueIsNotMutableAndCannotBeChanged : value - at Increment Mutable(Number) in TestPackage\Number.strict:line 37 - value = value + 1 - at Strict.Expressions.MutableReassignment..ctor(Body scope, Expression target, Expression newValue) in C:\code\GitHub\strict-lang\Strict\Strict.Expressions\MutableReassignment.cs:line 11 - at Strict.Expressions.MutableReassignment.TryParseReassignment(Body body, ReadOnlySpan`1 line) in C:\code\GitHub\strict-lang\Strict\Strict.Expressions\MutableReassignment.cs:line 44 - at Strict.Expressions.MutableReassignment.TryParse(Body body, ReadOnlySpan`1 line) in C:\code\GitHub\strict-lang\Strict\Strict.Expressions\MutableReassignment.cs:line 31 - at Strict.Language.Body.Parse() in C:\code\GitHub\strict-lang\Strict\Strict.Language\Body.cs:line 56 - at Strict.Language.Method.GetBodyAndParseIfNeeded(Boolean parseTestsOnlyForGeneric) in C:\code\GitHub\strict-lang\Strict\Strict.Language\Method.cs:line 383 - at Strict.HighLevelRuntime.Executor.Execute(Method method, ValueInstance instance, IReadOnlyList`1 args, ExecutionContext parentContext, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 50 - at Strict.HighLevelRuntime.Executor.Execute(Method method, ValueInstance instance, IReadOnlyList`1 args, ExecutionContext parentContext) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 18 - at Strict.HighLevelRuntime.MethodCallEvaluator.ExecuteMethodCall(MethodCall call, ValueInstance instance, ExecutionContext ctx) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\MethodCallEvaluator.cs:line 311 - at Strict.HighLevelRuntime.MethodCallEvaluator.Evaluate(MethodCall call, ExecutionContext ctx) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\MethodCallEvaluator.cs:line 32 - at Strict.HighLevelRuntime.Executor.EvaluateMethodCall(MethodCall call, ExecutionContext ctx) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 309 - at Strict.HighLevelRuntime.Executor.RunExpression(Expression expr, ExecutionContext context, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 238 - at Strict.HighLevelRuntime.IfEvaluator.Evaluate(If iff, ExecutionContext ctx) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\IfEvaluator.cs:line 12 - at Strict.HighLevelRuntime.Executor.RunExpression(Expression expr, ExecutionContext context, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 232 - at Strict.HighLevelRuntime.ForEvaluator.EvaluateBody(Body body, ExecutionContext ctx) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\ForEvaluator.cs:line 68 - at Strict.HighLevelRuntime.ForEvaluator.ExecuteForIteration(For f, ExecutionContext ctx, ValueInstance iterator, ICollection`1 results, Type itemType, Int32 index) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\ForEvaluator.cs:line 51 - at Strict.HighLevelRuntime.ForEvaluator.Evaluate(For f, ExecutionContext ctx) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\ForEvaluator.cs:line 32 - at Strict.HighLevelRuntime.Executor.RunExpression(Expression expr, ExecutionContext context, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 234 - at Strict.HighLevelRuntime.BodyEvaluator.TryEvaluate(Body body, ExecutionContext ctx, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\BodyEvaluator.cs:line 40 - at Strict.HighLevelRuntime.BodyEvaluator.Evaluate(Body body, ExecutionContext ctx, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\BodyEvaluator.cs:line 14 - at Strict.HighLevelRuntime.Executor.RunExpression(Expression expr, ExecutionContext context, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 227 - at Strict.HighLevelRuntime.Executor.Execute(Method method, ValueInstance instance, IReadOnlyList`1 args, ExecutionContext parentContext, Boolean runOnlyTests) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 62 - at Strict.HighLevelRuntime.Executor.Execute(Method method, ValueInstance instance, IReadOnlyList`1 args, ExecutionContext parentContext) in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime\Executor.cs:line 18 - at Strict.HighLevelRuntime.Tests.ForTests.RemoveParenthesesWithElseIfChain() in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime.Tests\ForTests.cs:line 139 - at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args) - at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr) - - - - - - - - - - - - - - - - - - - - - - NUnit Adapter 4.6.0.0: Test execution started -Running selected tests in C:\code\GitHub\strict-lang\Strict\Strict.HighLevelRuntime.Tests\bin\Debug\net10.0\Strict.HighLevelRuntime.Tests.dll - NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run -NUnit Adapter 4.6.0.0: Test execution complete - - - - \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/ToTests.cs b/Strict.HighLevelRuntime.Tests/ToTests.cs index a97e76cc..b8283bf8 100644 --- a/Strict.HighLevelRuntime.Tests/ToTests.cs +++ b/Strict.HighLevelRuntime.Tests/ToTests.cs @@ -8,7 +8,8 @@ namespace Strict.HighLevelRuntime.Tests; public sealed class ToTests { [SetUp] - public void CreateExecutor() => executor = new Executor(TestBehavior.Disabled); + public void CreateExecutor() => + executor = new Executor(TestPackage.Instance, TestBehavior.Disabled); private Executor executor = null!; @@ -22,11 +23,9 @@ public void EvaluateToTextAndNumber() using var t = CreateType(nameof(EvaluateToTextAndNumber), "has number", "GetText Text", "\tnumber to Text", "GetNumber Number", "\tnumber to Text to Number"); var instance = new ValueInstance(t, 5); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "GetText"), instance, []).Value, + Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "GetText"), instance, []).Text, Is.EqualTo("5")); - Assert.That( - Convert.ToDouble(executor. - Execute(t.Methods.Single(m => m.Name == "GetNumber"), instance, []).Value), + Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "GetNumber"), instance, []).Number, Is.EqualTo(5)); } @@ -35,7 +34,8 @@ public void ToCharacterComparison() { using var t = CreateType(nameof(ToCharacterComparison), "has number", "Compare", "\t5 to Character is \"5\""); - Assert.That(executor.Execute(t.Methods.Single(m => m.Name == "Compare"), null, []).Value, + Assert.That( + executor.Execute(t.Methods.Single(m => m.Name == "Compare"), executor.noneInstance, []).Boolean, Is.EqualTo(true)); } @@ -46,8 +46,7 @@ public void ConvertCharacterToNumberAndMultiply() new TypeLines(nameof(ConvertCharacterToNumberAndMultiply), "has character", "Convert(number)", "\tcharacter to Number * 10 ^ number")). ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That( - executor.Execute(type.Methods[0], new ValueInstance(type, '5'), - [new ValueInstance(type.GetType(Base.Number), 3)]).Value, Is.EqualTo(5 * 1000)); + Assert.That(executor.Execute(type.Methods[0], new ValueInstance(type, '5'), + [new ValueInstance(type.GetType(Type.Number), 3)]).Number, Is.EqualTo(5 * 1000)); } -} +} \ No newline at end of file diff --git a/Strict.HighLevelRuntime.Tests/ValueInstanceTests.cs b/Strict.HighLevelRuntime.Tests/ValueInstanceTests.cs deleted file mode 100644 index 3a397cbe..00000000 --- a/Strict.HighLevelRuntime.Tests/ValueInstanceTests.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Strict.Expressions; -using Strict.Language; -using Strict.Language.Tests; -using Type = Strict.Language.Type; - -namespace Strict.HighLevelRuntime.Tests; - -public sealed class ValueInstanceTests -{ - [SetUp] - public void CreateNumber() => numberType = TestPackage.Instance.GetType(Base.Number); - - private Type numberType = null!; - - [Test] - public void ToStringShowsTypeAndValue() => - Assert.That(new ValueInstance(numberType, 42).ToString(), Is.EqualTo("Number:42")); - - [Test] - public void CompareTwoNumbers() => - Assert.That(new ValueInstance(numberType, 42), Is.EqualTo(new ValueInstance(numberType, 42))); - - [Test] - public void CompareNumberToText() => - Assert.That(new ValueInstance(numberType, 5), - Is.Not.EqualTo(new ValueInstance(TestPackage.Instance.GetType(Base.Text), "5"))); - - [Test] - public void CompareLists() - { - var list = new ValueInstance(TestPackage.Instance.GetListImplementationType(numberType), - new[] { 1, 2, 3 }); - Assert.That(list, - Is.EqualTo(new ValueInstance(TestPackage.Instance.GetListImplementationType(numberType), - new[] { 1, 2, 3 }))); - Assert.That(list, - Is.Not.EqualTo(new ValueInstance(TestPackage.Instance.GetListImplementationType(numberType), - new[] { 1, 2, 1 }))); - Assert.That(list, - Is.Not.EqualTo(new ValueInstance(TestPackage.Instance.GetListImplementationType(numberType), - new[] { 1, 2, 3, 4 }))); - } - - [Test] - public void ListWithExpressionsThrows() - { - var listType = TestPackage.Instance.GetListImplementationType(numberType); - var expressions = new Expression[] { new Value(numberType, 1) }; - Assert.Throws(() => new ValueInstance(listType, expressions)); - } - - [Test] - public void GenericListTypeAcceptsValueInstances() - { - var listType = TestPackage.Instance.GetType("List(key Generic, mappedValue Generic)"); - Assert.That(new ValueInstance(listType, new List()), Is.Not.Null); - } - - [Test] - public void CompareDictionaries() - { - var list = new ValueInstance( - TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType), - new Dictionary { { 1, 2 } }); - Assert.That(list, - Is.EqualTo(new ValueInstance( - TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType), - new Dictionary { { 1, 2 } }))); - Assert.That(list, - Is.Not.EqualTo(new ValueInstance( - TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType), - new Dictionary { { 2, 2 } }))); - Assert.That(list, - Is.Not.EqualTo(new ValueInstance( - TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType), - new Dictionary { { 1, 3 } }))); - Assert.That(list, - Is.Not.EqualTo(new ValueInstance( - TestPackage.Instance.GetDictionaryImplementationType(numberType, numberType), - new Dictionary { { 1, 3 }, { 2, 2 } }))); - } - - [Test] - public void CompareTypeContainingNumber() - { - using var t = - new Type(TestPackage.Instance, - new TypeLines(nameof(CompareTypeContainingNumber), "has number", "Run Boolean", - "\tnumber is 42")).ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That(new ValueInstance(t, 42), Is.EqualTo(new ValueInstance(numberType, 42))); - } - - [Test] - public void NoneTypeRejectsNonNullValue() - { - var package = new Package(nameof(NoneTypeRejectsNonNullValue)); - var noneType = package.FindType(Base.None, package); - Assert.That(() => new ValueInstance(noneType!, 1), - Throws.InstanceOf()); - } - - [Test] - public void NonNoneTypeRejectsNullValue() => - Assert.That(() => new ValueInstance(numberType, null), - Throws.InstanceOf()); - - [Test] - public void BooleanTypeRejectsNonBoolValue() - { - var boolType = TestPackage.Instance.GetType(Base.Boolean); - Assert.That(() => new ValueInstance(boolType, 1), - Throws.InstanceOf()); - } - - [Test] - public void EnumTypeAllowsNumberAndText() - { - var enumType = new Type(TestPackage.Instance, - new TypeLines(nameof(EnumTypeAllowsNumberAndText), "constant One", "constant Something = \"Something\"")). - ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That(() => new ValueInstance(enumType, 1), Throws.Nothing); - } - - [Test] - public void EnumTypeRejectsNonEnumValue() - { - var enumType = new Type(TestPackage.Instance, - new TypeLines(nameof(EnumTypeRejectsNonEnumValue), "constant One")). - ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That(() => new ValueInstance(enumType, true), - Throws.InstanceOf()); - } - - [Test] - public void TextTypeRejectsNonStringValue() - { - var textType = TestPackage.Instance.GetType(Base.Text); - Assert.That(() => new ValueInstance(textType, 5), - Throws.InstanceOf()); - } - - [Test] - public void CharacterTypeRejectsNonCharacterValue() - { - var charType = TestPackage.Instance.GetType(Base.Character); - Assert.That(() => new ValueInstance(charType, "A"), - Throws.InstanceOf()); - } - - [Test] - public void ListTypeRejectsInvalidValue() - { - var listType = TestPackage.Instance.GetListImplementationType(numberType); - Assert.That(() => new ValueInstance(listType, new object()), - Throws.InstanceOf()); - } - - [Test] - public void NumberTypeRejectsNonNumericValue() => - Assert.That(() => new ValueInstance(numberType, "nope"), - Throws.InstanceOf()); - - [Test] - public void TypeRejectsUnknownDictionaryMembers() - { - using var t = new Type(TestPackage.Instance, - new TypeLines(nameof(TypeRejectsUnknownDictionaryMembers), "has number", "has text")). - ParseMembersAndMethods(new MethodExpressionParser()); - var values = new Dictionary { { "wrong", 1 } }; - Assert.That(() => new ValueInstance(t, values), - Throws.InstanceOf()); - } - - [Test] - public void NonErrorTypeRejectsUnsupportedValue() - { - using var t = new Type(TestPackage.Instance, - new TypeLines(nameof(NonErrorTypeRejectsUnsupportedValue), "has number", "has text")). - ParseMembersAndMethods(new MethodExpressionParser()); - Assert.That(() => new ValueInstance(t, new object()), - Throws.InstanceOf()); - } -} \ No newline at end of file diff --git a/Strict.HighLevelRuntime/BodyEvaluator.cs b/Strict.HighLevelRuntime/BodyEvaluator.cs index 0d6e1e50..54470b36 100644 --- a/Strict.HighLevelRuntime/BodyEvaluator.cs +++ b/Strict.HighLevelRuntime/BodyEvaluator.cs @@ -7,8 +7,9 @@ internal sealed class BodyEvaluator(Executor executor) { public ValueInstance Evaluate(Body body, ExecutionContext ctx, bool runOnlyTests) { + executor.Statistics.BodyCount++; if (runOnlyTests) - executor.IncrementInlineTestDepth(); + inlineTestDepth++; try { return TryEvaluate(body, ctx, runOnlyTests); @@ -17,49 +18,97 @@ public ValueInstance Evaluate(Body body, ExecutionContext ctx, bool runOnlyTests { throw new ExecutionFailed(body.Method, "Failed in \"" + body.Method.Type.FullName + "." + body.Method.Name + "\":" + - Environment.NewLine + body.Expressions.ToWordList(Environment.NewLine), ex); + Environment.NewLine + string.Join(Environment.NewLine, body.Expressions), ex); } finally { if (runOnlyTests) - executor.DecrementInlineTestDepth(); + inlineTestDepth--; } } + /// + /// Evaluate inline tests at top-level only (outermost call), avoid recursion + /// + internal int inlineTestDepth; + private ValueInstance TryEvaluate(Body body, ExecutionContext ctx, bool runOnlyTests) { - ValueInstance last = - new((ctx.This?.ReturnType.Package ?? body.Method.Type.Package).FindType(Base.None)!, null); - foreach (var e in body.Expressions) + var last = executor.noneInstance; + for (var index = 0; index < body.Expressions.Count; index++) { - var isTest = !e.Equals(body.Expressions[^1]) && IsStandaloneInlineTest(e); + var e = body.Expressions[index]; + var isTest = !ReferenceEquals(e, body.Expressions[^1]) && IsStandaloneInlineTest(e); + if (isTest) + executor.Statistics.TestExpressions++; if (isTest == !runOnlyTests && e is not Declaration && e is not MutableReassignment || - runOnlyTests && e is Declaration && - body.Method.Type.Members.Any(m => !m.IsConstant && e.ToString().Contains(m.Name))) + runOnlyTests && e is Declaration decl && DeclarationReferencesAnyMember(body, decl)) continue; last = executor.RunExpression(e, ctx); - if (runOnlyTests && isTest && !Executor.ToBool(last)) - throw new Executor.TestFailed(body.Method, e, last, GetTestFailureDetails(e, ctx)); + if (ctx.ExitMethodAndReturnValue.HasValue) + return ctx.ExitMethodAndReturnValue.Value; + if (runOnlyTests && isTest) + { + if (!last.Boolean) + throw new Executor.TestFailed(body.Method, e, last, GetTestFailureDetails(e, ctx)); + last = GetStandaloneInlineTestComparedValue(e, ctx) ?? last; + } } - if (runOnlyTests && last.Value == null && body.Method.Name != Base.Run && + if (runOnlyTests && last.Equals(executor.noneInstance) && body.Method.Name != Method.Run && body.Expressions.Count > 1) throw new Executor.MethodRequiresTest(body.Method, body); - if (runOnlyTests || last.ReturnType.IsError || body.Method.ReturnType == last.ReturnType) + if (runOnlyTests || last.IsError || last.IsType(body.Method.ReturnType)) return last; - if (body.Method.ReturnType.IsMutable && !last.ReturnType.IsMutable && - last.ReturnType == ((GenericTypeImplementation)body.Method.ReturnType). - ImplementationTypes[0]) - return new ValueInstance(body.Method.ReturnType, last.Value); + if (body.Method.ReturnType.IsMutable && !last.IsMutable && + last.IsType(((GenericTypeImplementation)body.Method.ReturnType).ImplementationTypes[0])) + return new ValueInstance(last, body.Method.ReturnType); throw new Executor.ReturnTypeMustMatchMethod(body, last); } + private ValueInstance? GetStandaloneInlineTestComparedValue(Expression expression, + ExecutionContext ctx) => expression switch + { + Binary { Method.Name: BinaryOperator.Is, Instance: Value v } => v.Data, + Binary { Method.Name: BinaryOperator.Is, Instance: not null } binary => + executor.RunExpression(binary.Instance, ctx), + Not { Instance: Binary { Method.Name: BinaryOperator.Is, Instance: Value v } } => v.Data, + Not { Instance: Binary { Method.Name: BinaryOperator.Is, Instance: not null } binary } => + executor.RunExpression(binary.Instance, ctx), //ncrunch: no coverage + _ => null + }; + + private static bool ExpressionReferencesMember(Expression expr, string memberName) => + expr switch + { + MemberCall m => m.Member.Name == memberName, + MethodCall call => + call.Instance != null && ExpressionReferencesMember(call.Instance, memberName) || + call.Arguments.Any(a => ExpressionReferencesMember(a, memberName)), + List list => list.Values.Any(v => ExpressionReferencesMember(v, memberName)), + Dictionary dict => + dict.KeyType.Name.Equals(memberName, StringComparison.OrdinalIgnoreCase) || + dict.MappedValueType.Name.Equals(memberName, StringComparison.OrdinalIgnoreCase), + _ => false + }; + private static bool IsStandaloneInlineTest(Expression e) => - e.ReturnType.Name == Base.Boolean && e is not If && e is not Return && e is not Declaration && + e.ReturnType.IsBoolean && e is not If && e is not Return && e is not Declaration && e is not MutableReassignment; + private static bool DeclarationReferencesAnyMember(Body body, Declaration decl) + { + var members = body.Method.Type.Members; + for (var i = 0; i < members.Count; i++) + if (!members[i].IsConstant && ExpressionReferencesMember(decl.Value, members[i].Name)) + return true; + return false; + } + private string GetTestFailureDetails(Expression expression, ExecutionContext ctx) => - expression is Binary { Method.Name: BinaryOperator.Is, Instance: not null } binary && - binary.Arguments.Count == 1 + expression is Binary + { + Method.Name: BinaryOperator.Is, Instance: not null, Arguments.Count: 1 + } binary ? GetBinaryComparisonDetails(binary, ctx, BinaryOperator.Is) : expression is Not { Instance: Binary { Method.Name: BinaryOperator.Is } notBinary } && notBinary.Arguments.Count == 1 diff --git a/Strict.HighLevelRuntime/ExecutionContext.cs b/Strict.HighLevelRuntime/ExecutionContext.cs index 0875a3a4..7fc5cb62 100644 --- a/Strict.HighLevelRuntime/ExecutionContext.cs +++ b/Strict.HighLevelRuntime/ExecutionContext.cs @@ -1,5 +1,5 @@ +using Strict.Expressions; using Strict.Language; -using System.Collections; using Type = Strict.Language.Type; namespace Strict.HighLevelRuntime; @@ -10,25 +10,43 @@ public sealed class ExecutionContext(Type type, Method method) public Method Method { get; } = method; public ExecutionContext? Parent { get; init; } public ValueInstance? This { get; init; } - public Dictionary Variables { get; } = new(StringComparer.Ordinal); + private Dictionary? variables; + /// + /// Lazy-initialized: only created when a variable is actually written, avoiding allocation for + /// methods that never declare local variables (the majority of test-runner invocations). + /// + public Dictionary Variables => + variables ??= new Dictionary(StringComparer.Ordinal); + public ValueInstance? ExitMethodAndReturnValue { get; internal set; } - public ValueInstance Get(string name) => - Find(name) ?? throw new VariableNotFound(name, Type, This); + public ValueInstance Get(string name, Statistics statistics) => + Find(name, statistics) ?? throw new VariableNotFound(name, Type, This); - public ValueInstance? Find(string name) + public ValueInstance? Find(string name, Statistics statistics) { - if (Variables.TryGetValue(name, out var v)) + statistics.FindVariableCount++; + if (variables != null && variables.TryGetValue(name, out var v)) return v; if (This == null) - return Parent?.Find(name); + return Parent?.Find(name, statistics); if (name == Type.ValueLowercase) return This; - var implicitMember = - Type.Members.FirstOrDefault(m => !m.IsConstant && m.Type.Name != Base.Iterator); - if (implicitMember != null && - implicitMember.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - return new ValueInstance(implicitMember.Type, This.Value); - return Parent?.Find(name); + var members = Type.Members; + for (var i = 0; i < members.Count; i++) + if (!members[i].IsConstant && members[i].Type.Name != Type.Iterator && + members[i].Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + return new ValueInstance(This.Value, members[i].Type); + return Parent?.Find(name, statistics); + } + + /// + /// Clears local variables and resets the early-exit so the same context can be reused in the + /// next for iteration, saving and context allocations. + /// + public void ResetIteration() + { + variables?.Clear(); + ExitMethodAndReturnValue = null; } public ValueInstance Set(string name, ValueInstance value) @@ -36,25 +54,13 @@ public ValueInstance Set(string name, ValueInstance value) var ctx = this; while (ctx != null) { - if (ctx.Variables.ContainsKey(name)) - return ctx.Variables[name] = value; + if (ctx.variables != null && ctx.variables.ContainsKey(name)) + return ctx.variables[name] = value; ctx = ctx.Parent; } return Variables[name] = value; } - internal static IList BuildDictionaryPairsList(Type listMemberType, - Dictionary dictionary) - { - var elementType = listMemberType is GenericTypeImplementation { Generic.Name: Base.List } list - ? list.ImplementationTypes[0] - : listMemberType; - var pairs = new List(dictionary.Count); - foreach (var entry in dictionary) - pairs.Add(new ValueInstance(elementType, new List { entry.Key, entry.Value })); - return pairs; - } - public sealed class VariableNotFound(string name, Type type, ValueInstance? instance) : Exception($"Variable '{name}' or member '{name}' of this type '{type}'" + (instance != null ? $" (instance='{instance}')" @@ -63,5 +69,5 @@ public sealed class VariableNotFound(string name, Type type, ValueInstance? inst public override string ToString() => nameof(ExecutionContext) + " Type=" + Type.Name + ", This=" + This + ", Variables:" + Environment.NewLine + " " + - Variables.DictionaryToWordList(Environment.NewLine + " ", " ", true); + (variables?.DictionaryToWordList(Environment.NewLine + " ", " ", true) ?? ""); } \ No newline at end of file diff --git a/Strict.HighLevelRuntime/Executor.cs b/Strict.HighLevelRuntime/Executor.cs index f2f14815..0593e2fd 100644 --- a/Strict.HighLevelRuntime/Executor.cs +++ b/Strict.HighLevelRuntime/Executor.cs @@ -1,84 +1,99 @@ using Strict.Expressions; using Strict.Language; -using System.Collections; +using System.Runtime.CompilerServices; using Type = Strict.Language.Type; +[assembly: InternalsVisibleTo("Strict.HighLevelRuntime.Tests")] +[assembly: InternalsVisibleTo("Strict.TestRunner")] + namespace Strict.HighLevelRuntime; -public sealed class Executor(TestBehavior behavior = TestBehavior.OnFirstRun) +public class Executor { - public ValueInstance Execute(Method method, ValueInstance? instance, - IReadOnlyList args, ExecutionContext? parentContext = null) + public Executor(Package initialPackage, TestBehavior behavior = TestBehavior.OnFirstRun) { - ValueInstance? returnValue = null; - if (inlineTestDepth == 0 && behavior != TestBehavior.Disabled && + this.behavior = behavior; + noneType = initialPackage.GetType(Type.None); + noneInstance = new ValueInstance(noneType); + booleanType = initialPackage.GetType(Type.Boolean); + trueInstance = new ValueInstance(booleanType, true); + falseInstance = new ValueInstance(booleanType, false); + numberType = initialPackage.GetType(Type.Number); + characterType = initialPackage.GetType(Type.Character); + rangeType = initialPackage.GetType(Type.Range); + listType = initialPackage.GetType(Type.List); + bodyEvaluator = new BodyEvaluator(this); + ifEvaluator = new IfEvaluator(this); + selectorIfEvaluator = new SelectorIfEvaluator(this); + forEvaluator = new ForEvaluator(this); + methodCallEvaluator = new MethodCallEvaluator(this); + toEvaluator = new ToEvaluator(this); + } + + private readonly TestBehavior behavior; + internal readonly Type noneType; + internal readonly ValueInstance noneInstance; + internal readonly Type booleanType; + internal readonly ValueInstance trueInstance; + internal readonly ValueInstance falseInstance; + internal readonly Type numberType; + internal readonly Type characterType; + internal readonly Type rangeType; + internal readonly Type listType; + private readonly BodyEvaluator bodyEvaluator; + private readonly IfEvaluator ifEvaluator; + private readonly SelectorIfEvaluator selectorIfEvaluator; + private readonly ForEvaluator forEvaluator; + private readonly MethodCallEvaluator methodCallEvaluator; + private readonly ToEvaluator toEvaluator; + + public ValueInstance Execute(Method method) + { + var returnValue = noneInstance; + if (bodyEvaluator.inlineTestDepth == 0 && behavior != TestBehavior.Disabled && (behavior == TestBehavior.TestRunner || validatedMethods.Add(method))) - returnValue = Execute(method, instance, args, parentContext, true); - if (inlineTestDepth > 0 || behavior != TestBehavior.TestRunner) - returnValue = Execute(method, instance, args, parentContext, false); - return returnValue ?? new ValueInstance(method.ReturnType, null); + returnValue = Execute(method, noneInstance, [], null, true); + if (bodyEvaluator.inlineTestDepth > 0 || behavior != TestBehavior.TestRunner) + returnValue = Execute(method, noneInstance, []); + return returnValue; } - /// - /// Evaluate inline tests at top-level only (outermost call), avoid recursion - /// - private int inlineTestDepth; private readonly HashSet validatedMethods = []; - private BodyEvaluator BodyEvaluator => field ??= new BodyEvaluator(this); - private IfEvaluator IfEvaluator => field ??= new IfEvaluator(this); - private SelectorIfEvaluator SelectorIfEvaluator => field ??= new SelectorIfEvaluator(this); - private ForEvaluator ForEvaluator => field ??= new ForEvaluator(this); - private MethodCallEvaluator MethodCallEvaluator => field ??= new MethodCallEvaluator(this); - private ToEvaluator ToEvaluator => field ??= new ToEvaluator(this); - - private ValueInstance Execute(Method method, ValueInstance? instance, - IReadOnlyList args, ExecutionContext? parentContext, bool runOnlyTests) + public Statistics Statistics { get; } = new(); + + public ValueInstance Execute(Method method, ValueInstance instance, + IReadOnlyList args, ExecutionContext? parentContext = null, bool runOnlyTests = false) { + Statistics.MethodCount++; ValidateInstanceAndArguments(method, instance, args, parentContext); if (method is { Name: Method.From, Type.IsGeneric: false }) - return instance != null - ? throw new MethodCall.CannotCallFromConstructorWithExistingInstance() - : new ValueInstance(method.Type, GetFromConstructorValue(method, args)); + return instance.Equals(noneInstance) + ? GetFromConstructorValue(method, args) + : throw new MethodCall.CannotCallFromConstructorWithExistingInstance(); + if (runOnlyTests && IsSimpleSingleLineMethod(method)) + return trueInstance; var context = CreateExecutionContext(method, instance, args, parentContext, runOnlyTests); + Expression body; try { - if (runOnlyTests && IsSimpleSingleLineMethod(method)) - return Bool(method.ReturnType, true); - Expression body; - try - { - body = method.GetBodyAndParseIfNeeded(runOnlyTests && method.Type.IsGeneric); - } - catch (Exception inner) when (runOnlyTests) - { - throw new MethodRequiresTest(method, - $"Test execution failed: {method.Parent.FullName}.{method.Name}\n" + - method.lines.ToWordList("\n") + "\n" + inner); - } - if (body is not Body && runOnlyTests) - return IsSimpleExpressionWithLessThanThreeSubExpressions(body) - ? Bool(body.ReturnType, true) - : throw new MethodRequiresTest(method, body.ToString()); - var result = RunExpression(body, context, runOnlyTests); - if (!runOnlyTests && method.ReturnType is GenericTypeImplementation - { - Generic.Name: Base.Mutable - } mutableReturnType && !result.ReturnType.IsMutable && - result.ReturnType.IsSameOrCanBeUsedAs(mutableReturnType.ImplementationTypes[0])) - return new ValueInstance(method.ReturnType, result.Value); - return !runOnlyTests && !method.ReturnType.IsMutable && result.ReturnType.IsMutable && - ((GenericTypeImplementation)result.ReturnType).ImplementationTypes[0]. - IsSameOrCanBeUsedAs(method.ReturnType) - ? new ValueInstance(method.ReturnType, result.Value) - : result; + body = method.GetBodyAndParseIfNeeded(runOnlyTests && method.Type.IsGeneric); } - catch (ReturnSignal ret) + catch (Exception inner) when (runOnlyTests) { - return ret.Value; + throw new MethodRequiresTest(method, + $"Test execution failed: {method.Parent.FullName}.{method.Name}\n" + + method.lines.ToLines() + Environment.NewLine + inner); } + if (body is not Body && runOnlyTests) + return IsSimpleExpressionWithLessThanThreeSubExpressions(body) + ? trueInstance + : throw new MethodRequiresTest(method, body.ToString()); + var result = RunExpression(body, context, runOnlyTests); + return context.ExitMethodAndReturnValue ?? + result.ApplyMethodReturnTypeMutable(method.ReturnType); } - private ExecutionContext CreateExecutionContext(Method method, ValueInstance? instance, + private ExecutionContext CreateExecutionContext(Method method, ValueInstance instance, IReadOnlyList args, ExecutionContext? parentContext, bool runOnlyTests) { var context = @@ -94,34 +109,31 @@ private ExecutionContext CreateExecutionContext(Method method, ValueInstance? in : throw new MissingArgument(method, param.Name, args); context.Variables[param.Name] = arg; } - AddDictionaryElementsAlias(context, instance); return context; } - private static void ValidateInstanceAndArguments(Method method, ValueInstance? instance, + private void ValidateInstanceAndArguments(Method method, ValueInstance instance, IReadOnlyList args, ExecutionContext? parentContext) { - if (instance != null && !instance.ReturnType.IsSameOrCanBeUsedAs(method.Type)) - throw new CannotCallMethodWithWrongInstance(method, instance); //ncrunch: no coverage + if (!instance.IsPrimitiveType(noneType) && !instance.IsSameOrCanBeUsedAs(method.Type)) + throw new CannotCallMethodWithWrongInstance(method, instance); if (args.Count > method.Parameters.Count) throw new TooManyArguments(method, args[method.Parameters.Count].ToString(), args); for (var index = 0; index < args.Count; index++) - if (!args[index].ReturnType.IsSameOrCanBeUsedAs(method.Parameters[index].Type) && + if (!args[index].IsSameOrCanBeUsedAs(method.Parameters[index].Type) && !method.Parameters[index].Type.IsIterator && method.Name != Method.From && !IsSingleCharacterTextArgument(method.Parameters[index].Type, args[index])) throw new ArgumentDoesNotMapToMethodParameters(method, "Method \"" + method + "\" parameter " + index + ": " + method.Parameters[index].ToStringWithInnerMembers() + - " cannot be assigned from argument " + args[index] + " " + args[index].ReturnType); + " cannot be assigned from argument " + args[index]); if (parentContext != null && parentContext.Method == method && - (parentContext.This?.Equals(instance) ?? instance == null) && + (parentContext.This?.Equals(instance) ?? instance.Equals(noneInstance)) && DoArgumentsMatch(method, args, parentContext.Variables)) throw new StackOverflowCallingItselfWithSameInstanceAndArguments(method, instance, args, parentContext); } - internal static ValueInstance Bool(Context any, bool b) => new(any.GetType(Base.Boolean), b); - private static bool DoArgumentsMatch(Method method, IReadOnlyList args, IReadOnlyDictionary parentContextVariables) { @@ -133,196 +145,184 @@ private static bool DoArgumentsMatch(Method method, IReadOnlyList } public sealed class StackOverflowCallingItselfWithSameInstanceAndArguments(Method method, - ValueInstance? instance, IEnumerable args, ExecutionContext parentContext) + ValueInstance? instance, IReadOnlyList args, ExecutionContext parentContext) : ExecutionFailed(method, "Parent context=" + parentContext + ", Instance=" + instance + - ", arguments=" + args.ToWordList()); + ", arguments=" + string.Join(", ", args)); - private static IDictionary ConvertFromArgumentsToDictionary(Method fromMethod, - IReadOnlyList args) + private ValueInstance GetFromConstructorValue(Method method, IReadOnlyList args) { - var type = fromMethod.Type; - var result = new Dictionary(StringComparer.Ordinal); + Statistics.FromCreationsCount++; + if (args.Count == 0 && method.Type.IsText) + return new ValueInstance(""); + if ((method.Type.IsCharacter || method.Type.IsNumber || method.Type.IsEnum) && args.Count == 1) + { + if (IsSingleCharacterTextArgument(method.Type, args[0])) + return new ValueInstance(method.Type, args[0].Text[0]); + if (!args[0].IsText || args[0].IsSameOrCanBeUsedAs(method.Type)) + return new ValueInstance(method.Type, args[0].Number); + } //ncrunch: no coverage + if (method.Type.IsList) + return new ValueInstance(method.Type, args); + if (method.Type.IsDictionary) + return args[0].IsDictionary + ? args[0] + : new ValueInstance(method.Type, FillDictionaryFromListKeyAndValues(args[0])); + var members = new Dictionary(StringComparer.Ordinal); for (var index = 0; index < args.Count; index++) { - var parameter = fromMethod.Parameters[index]; - if (!args[index].ReturnType.IsSameOrCanBeUsedAs(parameter.Type) && + var parameter = method.Parameters[index]; + if (!args[index].IsSameOrCanBeUsedAs(parameter.Type) && !parameter.Type.IsIterator && !IsSingleCharacterTextArgument(parameter.Type, args[index])) - throw new InvalidTypeForArgument(type, args, index); - var memberName = type.Members.FirstOrDefault(member => - member.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase))?.Name ?? - (parameter.Name.Length > 0 - ? char.ToUpperInvariant(parameter.Name[0]) + parameter.Name[1..] - : parameter.Name); - result.Add(memberName, TryConvertSingleCharacterText(parameter.Type, args[index])); + throw new InvalidTypeForArgument(method.Type, args, index); + var memberName = GetFirstMemberMatchingParameter(method, parameter)?.Name ?? + parameter.Name.MakeFirstLetterUppercase(); + members.Add(memberName, IsSingleCharacterTextArgument(parameter.Type, args[index]) + ? new ValueInstance(characterType, args[index].Text[0]) + : args[index]); } - return result; + return new ValueInstance(method.Type, members); } - private static object? GetFromConstructorValue(Method method, IReadOnlyList args) + private static Member? GetFirstMemberMatchingParameter(Method method, Parameter parameter) { - if (args.Count == 0 && method.Type.Name == Base.Text) - return ""; - if (method.Type.IsDictionary && args.Count == 1 && args[0].ReturnType.IsIterator) - return args[0].Value as IDictionary ?? FillDictionaryFromListKeyAndValues(args[0].Value); - if (args.Count == 1) - { - var arg = args[0]; - if (method.Type.Name == Base.Character && IsSingleCharacterTextArgument(method.Type, arg)) - return (int)((string)arg.Value!)[0]; - if (method.Type.IsSameOrCanBeUsedAs(arg.ReturnType) && - !IsSingleCharacterTextArgument(method.Type, arg)) - return arg.Value; - } - return ConvertFromArgumentsToDictionary(method, args); + foreach (var member in method.Type.Members) + if (member.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase)) + return member; + return null; //ncrunch: no coverage } - private static void AddDictionaryElementsAlias(ExecutionContext context, ValueInstance? instance) + private static Dictionary FillDictionaryFromListKeyAndValues( + ValueInstance value) { - if (instance?.ReturnType is not GenericTypeImplementation - { - Generic.Name: Base.Dictionary - } implementation || instance.Value is not Dictionary dictionary || - context.Variables.ContainsKey(Type.ElementsLowercase)) - return; - var listMemberType = implementation.Members.FirstOrDefault(member => - member.Type is GenericTypeImplementation { Generic.Name: Base.List } || - member.Type.Name == Base.List)?.Type ?? implementation.GetType(Base.List); - var listValue = ExecutionContext.BuildDictionaryPairsList(listMemberType, dictionary); - context.Set(Type.ElementsLowercase, new ValueInstance(listMemberType, listValue)); + var dictionary = new Dictionary(); + foreach (var pair in value.List.Items) + { + var keyAndValue = pair.List.Items; + dictionary[keyAndValue[0]] = keyAndValue[1]; + } + return dictionary; } - private static object? TryConvertSingleCharacterText(Type targetType, ValueInstance value) => - value.ReturnType.Name == Base.Text && value.Value is string { Length: 1 } text && - targetType.Name is Base.Number or Base.Character - ? (int)text[0] - : value.Value; - private static bool IsSingleCharacterTextArgument(Type targetType, ValueInstance value) => - value.ReturnType.Name == Base.Text && value.Value is string text && text.Length == 1 && - targetType.Name is Base.Number or Base.Character; + value is { IsText: true, Text.Length: 1 } && (targetType.IsNumber || targetType.IsCharacter); - public sealed class InvalidTypeForArgument( - Type type, - IReadOnlyList args, - int index) : ExecutionFailed(type, - args[index] + " at index=" + index + " does not match type=" + type + " Member=" + - type.Members[index]); + public sealed class InvalidTypeForArgument(Type type, IReadOnlyList args, + int index) : ExecutionFailed(type, args[index] + " at index=" + index + " does not match " + + "type=" + type + " Member=" + type.Members[index]); public sealed class CannotCallMethodWithWrongInstance(Method method, ValueInstance instance) : ExecutionFailed(method, instance.ToString()); //ncrunch: no coverage - public sealed class - TooManyArguments(Method method, string argument, IEnumerable args) - : ExecutionFailed(method, - argument + ", given arguments: " + args.ToWordList() + ", method " + method.Name + - " requires these parameters: " + method.Parameters.ToWordList()); + public sealed class TooManyArguments(Method method, string argument, + IReadOnlyList args) : ExecutionFailed(method, + argument + ", given arguments: " + string.Join(", ", args) + ", method " + method.Name + + " requires these parameters: " + string.Join(", ", method.Parameters)); public sealed class ArgumentDoesNotMapToMethodParameters(Method method, string message) : ExecutionFailed(method, message); - public sealed class - MissingArgument(Method method, string paramName, IEnumerable args) - : ExecutionFailed(method, - paramName + ", given arguments: " + args.ToWordList() + ", method " + method.Name + - " requires these parameters: " + method.Parameters.ToWordList()); + public sealed class MissingArgument(Method method, string paramName, + IReadOnlyList args) : ExecutionFailed(method, + paramName + ", given arguments: " + string.Join(", ", args) + ", method " + method.Name + + " requires these parameters: " + string.Join(", ", method.Parameters)); public ValueInstance RunExpression(Expression expr, ExecutionContext context, - bool runOnlyTests = false) => - expr switch + bool runOnlyTests = false) + { + Statistics.ExpressionCount++; + return expr switch { - Body body => BodyEvaluator.Evaluate(body, context, runOnlyTests), - Value v => v.Data as ValueInstance ?? CreateValueInstance(v.ReturnType, v.Data, context), + Body body => bodyEvaluator.Evaluate(body, context, runOnlyTests), + List list => EvaluateListExpression(list, context), + Dictionary dict => dict.Data, + Value v => v.Data, ParameterCall or VariableCall => EvaluateVariable(expr.ToString(), context), MemberCall m => EvaluateMemberCall(m, context), - ListCall listCall => MethodCallEvaluator.EvaluateListCall(listCall, context), - If iff => IfEvaluator.Evaluate(iff, context), - SelectorIf selectorIf => SelectorIfEvaluator.Evaluate(selectorIf, context), - For f => ForEvaluator.Evaluate(f, context), + ListCall listCall => methodCallEvaluator.EvaluateListCall(listCall, context), + If iff => ifEvaluator.Evaluate(iff, context), + SelectorIf selectorIf => selectorIfEvaluator.Evaluate(selectorIf, context), + For f => forEvaluator.Evaluate(f, context), Return r => EvaluateReturn(r, context), - To t => ToEvaluator.Evaluate(t, context), + To t => toEvaluator.Evaluate(t, context), Not n => EvaluateNot(n, context), MethodCall call => EvaluateMethodCall(call, context), - Declaration c => EvaluateAndAssign(c.Name, c.Value, context), - MutableReassignment a => EvaluateAndAssign(a.Name, a.Value, context), + Declaration c => EvaluateAndAssign(c.Name, c.Value, context, true), + MutableReassignment a => EvaluateAndAssign(a.Name, a.Value, context, false), Instance => EvaluateVariable(Type.ValueLowercase, context), _ => throw new ExpressionNotSupported(expr, context) //ncrunch: no coverage }; - - private object EvaluateValueData(object valueData, ExecutionContext context) => - valueData switch - { - Expression valueExpression => RunExpression(valueExpression, context), //ncrunch: no coverage - IList list => list.Select(e => RunExpression(e, context)).ToList(), - _ => valueData - }; - - private ValueInstance CreateValueInstance(Type returnType, object valueData, - ExecutionContext context) - { - var value = EvaluateValueData(valueData, context); - if (returnType.IsDictionary) - value = NormalizeDictionaryValue(returnType, (IDictionary)value); - return new ValueInstance(returnType, value); } - private static object NormalizeDictionaryValue(Type dictionaryType, - IDictionary rawMembers) + private ValueInstance EvaluateListExpression(List list, ExecutionContext context) { - var listMemberName = dictionaryType.Members.First(member => - member.Type is GenericTypeImplementation { Generic.Name: Base.List } || - member.Type.Name == Base.List).Name; - return FillDictionaryFromListKeyAndValues(rawMembers[listMemberName]); - } - - private static object FillDictionaryFromListKeyAndValues(object? value) - { - var pairs = (IList)value!; - var dictionary = new Dictionary(); - foreach (var pair in pairs) - { - var keyAndValue = (List)((ValueInstance)pair!).Value!; - dictionary[keyAndValue[0]] = keyAndValue[1]; - } - return dictionary; + var values = new List(list.Values.Count); + foreach (var value in list.Values) + values.Add(RunExpression(value, context)); + return new ValueInstance(list.ReturnType, values); } public class ExpressionNotSupported(Expression expr, ExecutionContext context) : ExecutionFailed(context.Type, expr.GetType().Name); //ncrunch: no coverage - private static ValueInstance EvaluateVariable(string name, ExecutionContext context) => - context.Find(name) ?? (name == Type.ValueLowercase + private ValueInstance EvaluateVariable(string name, ExecutionContext context) + { + Statistics.VariableCallCount++; + return context.Find(name, Statistics) ?? (name == Type.ValueLowercase ? context.This : null) ?? throw new ExecutionContext.VariableNotFound(name, context.Type, context.This); + } public ValueInstance EvaluateMemberCall(MemberCall member, ExecutionContext ctx) { + Statistics.MemberCallCount++; if (ctx.This == null && ctx.Type.Members.Contains(member.Member)) throw new UnableToCallMemberWithoutInstance(member, ctx); //ncrunch: no coverage - if (ctx.This?.Value is Dictionary dict && - dict.TryGetValue(member.Member.Name, out var value)) - return new ValueInstance(member.ReturnType, value); + if (ctx.This is { IsDictionary: true } && + member.Member.Name.Equals(Type.ElementsLowercase, StringComparison.OrdinalIgnoreCase)) + { + var pairs = new List(); + var pairType = member.Member.Type is { IsList: true, IsGeneric: true } + ? listType.GetFirstImplementation() + : member.Member.Type; + foreach (var pair in ctx.This.Value.GetDictionaryItems()) + pairs.Add(new ValueInstance(pairType, [pair.Key, pair.Value])); + return new ValueInstance(member.Member.Type, pairs); + } + var typeInstance = ctx.This?.TryGetValueTypeInstance(); + if (typeInstance != null && typeInstance.Members.TryGetValue(member.Member.Name, out var value)) + return value; if (member.Member.InitialValue != null && member.IsConstant) return RunExpression(member.Member.InitialValue, ctx); return member.Instance is VariableCall { Variable.Name: Type.OuterLowercase } - ? ctx.Parent!.Get(member.Member.Name) - : ctx.Get(member.Member.Name); + ? ctx.Parent!.Get(member.Member.Name, Statistics) + : ctx.Get(member.Member.Name, Statistics); } - internal void IncrementInlineTestDepth() => inlineTestDepth++; - internal void DecrementInlineTestDepth() => inlineTestDepth--; + public class UnableToCallMemberWithoutInstance(MemberCall member, ExecutionContext ctx) + : Exception(member + ", context " + ctx); //ncrunch: no coverage internal ValueInstance EvaluateMethodCall(MethodCall call, ExecutionContext ctx) => - MethodCallEvaluator.Evaluate(call, ctx); + methodCallEvaluator.Evaluate(call, ctx); public sealed class ReturnTypeMustMatchMethod(Body body, ValueInstance last) : ExecutionFailed( - body.Method, - "Return value " + last + " does not match method " + body.Method.Name + " ReturnType=" + - body.Method.ReturnType); + body.Method, "Return value " + last + " does not match method " + body.Method.Name + + " ReturnType=" + body.Method.ReturnType); + + private readonly Dictionary simpleMethodCache = new(); /// /// Skip parsing for trivially simple methods during validation to avoid missing-instance errors. /// - private static bool IsSimpleSingleLineMethod(Method method) + private bool IsSimpleSingleLineMethod(Method method) + { + if (simpleMethodCache.TryGetValue(method, out var cached)) + return cached; + var result = CheckIsSimpleSingleLineMethod(method); + simpleMethodCache[method] = result; + return result; + } + + private static bool CheckIsSimpleSingleLineMethod(Method method) { if (method.lines.Count != 2) return false; @@ -331,10 +331,29 @@ private static bool IsSimpleSingleLineMethod(Method method) if (hasMethodCalls) return false; var thenCount = CountThenSeparators(bodyLine); - var operatorCount = bodyLine.Split(' ').Count(w => w is "and" or "or" or "not" or "is"); + var operatorCount = CountOperatorWords(bodyLine); return thenCount == 0 && operatorCount <= 1 || thenCount == 1 && operatorCount <= 2; } + private static int CountOperatorWords(string input) + { + var span = input.AsSpan(); + var count = 0; + while (span.Length > 0) + { + var spaceIndex = span.IndexOf(' '); + var word = spaceIndex < 0 + ? span + : span[..spaceIndex]; + if (word is "and" or "or" or "not" or "is") + count++; + if (spaceIndex < 0) + break; + span = span[(spaceIndex + 1)..]; + } + return count; + } + private static int CountThenSeparators(string input) { var count = 0; @@ -380,36 +399,43 @@ public MethodRequiresTest(Method method, Body body) : this(method, body + " ({CountExpressionComplexity(body)} expressions)") { } } - public sealed class TestFailed(Method method, Expression expression, ValueInstance result, + public sealed class TestFailed(Method method, Expression expression, ValueInstance result, string details) : ExecutionFailed(method, $"\"{method.Name}\" method failed: {expression}, result: {result}" + (details.Length > 0 ? $", evaluated: {details}" : "") + " in" + Environment.NewLine + $"{method.Type.FilePath}:line {expression.LineNumber + 1}"); - private ValueInstance EvaluateAndAssign(string name, Expression value, ExecutionContext ctx) => - ctx.Set(name, RunExpression(value, ctx)); - - private ValueInstance EvaluateReturn(Return r, ExecutionContext ctx) => - throw new ReturnSignal(RunExpression(r.Value, ctx)); - - private ValueInstance EvaluateNot(Not not, ExecutionContext ctx) => - Bool(not.ReturnType, !ToBool(RunExpression(not.Instance!, ctx))); - - public class UnableToCallMemberWithoutInstance(MemberCall member, ExecutionContext ctx) - : Exception(member + ", context " + ctx); //ncrunch: no coverage - - internal static bool ToBool(object? v) => - v switch + private ValueInstance EvaluateAndAssign(string name, Expression value, ExecutionContext ctx, + bool isDeclaration) + { + if (isDeclaration) + Statistics.VariableDeclarationCount++; + if (value.IsMutable) { - bool b => b, - ValueInstance vi => ToBool(vi.Value), - Value { ReturnType.Name: Base.Boolean, Data: bool bv } => bv, //ncrunch: no coverage - _ => throw new InvalidOperationException("Expected Boolean, got: " + v) //ncrunch: no coverage - }; + if (isDeclaration) + Statistics.MutableDeclarationCount++; + Statistics.MutableUsageCount++; + } + return ctx.Set(name, RunExpression(value, ctx)); + } - private sealed class ReturnSignal(ValueInstance value) : Exception + private ValueInstance EvaluateReturn(Return r, ExecutionContext ctx) { - public ValueInstance Value { get; } = value; + Statistics.ReturnCount++; + var result = RunExpression(r.Value, ctx); + ctx.ExitMethodAndReturnValue = result; + return result; } + + private ValueInstance EvaluateNot(Not not, ExecutionContext ctx) + { + Statistics.UnaryCount++; + return ToBoolean(!RunExpression(not.Instance!, ctx).Boolean); + } + + public ValueInstance ToBoolean(bool isTrue) => + isTrue + ? trueInstance + : falseInstance; } \ No newline at end of file diff --git a/Strict.HighLevelRuntime/ForEvaluator.cs b/Strict.HighLevelRuntime/ForEvaluator.cs index 70b9302e..522c09c2 100644 --- a/Strict.HighLevelRuntime/ForEvaluator.cs +++ b/Strict.HighLevelRuntime/ForEvaluator.cs @@ -1,5 +1,6 @@ using Strict.Expressions; using Strict.Language; +using System.Text; using Type = Strict.Language.Type; namespace Strict.HighLevelRuntime; @@ -8,93 +9,143 @@ internal sealed class ForEvaluator(Executor executor) { public ValueInstance Evaluate(For f, ExecutionContext ctx) { + executor.Statistics.ForCount++; var iterator = executor.RunExpression(f.Iterator, ctx); - var results = new List(); + List? results = null; var itemType = GetForValueType(iterator); - if (iterator.ReturnType.Name == Base.Range && - iterator.Value is IDictionary rangeValues && - rangeValues.TryGetValue("Start", out var startValue) && - rangeValues.TryGetValue("ExclusiveEnd", out var endValue)) + var iteratorInstance = iterator.TryGetValueTypeInstance(); + var loop = new ExecutionContext(ctx.Type, ctx.Method) { This = ctx.This, Parent = ctx }; + if (iteratorInstance?.ReturnType == executor.rangeType && + iteratorInstance.Members.TryGetValue("Start", out var startValue) && + iteratorInstance.Members.TryGetValue("ExclusiveEnd", out var endValue)) { - var start = Convert.ToInt32(startValue); - var end = Convert.ToInt32(endValue); + var start = (int)startValue.Number; + var end = (int)endValue.Number; if (start <= end) for (var index = start; index < end; index++) - ExecuteForIteration(f, ctx, iterator, results, itemType, index); + { + loop.ResetIteration(); + ExecuteForIteration(f, ctx, iterator, ref results, itemType, index, loop); + if (ctx.ExitMethodAndReturnValue.HasValue) + return ctx.ExitMethodAndReturnValue.Value; + } else for (var index = start; index > end; index--) - ExecuteForIteration(f, ctx, iterator, results, itemType, index); + { + loop.ResetIteration(); + ExecuteForIteration(f, ctx, iterator, ref results, itemType, index, loop); + if (ctx.ExitMethodAndReturnValue.HasValue) + return ctx.ExitMethodAndReturnValue.Value; + } } else { var loopRange = new Range(0, iterator.GetIteratorLength()); for (var index = loopRange.Start.Value; index < loopRange.End.Value; index++) - ExecuteForIteration(f, ctx, iterator, results, itemType, index); + { + loop.ResetIteration(); + ExecuteForIteration(f, ctx, iterator, ref results, itemType, index, loop); + if (ctx.ExitMethodAndReturnValue.HasValue) + return ctx.ExitMethodAndReturnValue.Value; + } } - return ShouldConsolidateForResult(results, ctx) ?? new ValueInstance(results.Count == 0 - ? iterator.ReturnType - : iterator.ReturnType.GetType(Base.List).GetGenericImplementation(results[0].ReturnType), - results); + return ShouldConsolidateForResult(results, ctx) ?? new ValueInstance( + executor.listType.GetGenericImplementation(itemType), results ?? []); } private void ExecuteForIteration(For f, ExecutionContext ctx, ValueInstance iterator, - ICollection results, Type itemType, int index) + ref List? results, Type itemType, int index, ExecutionContext loop) { - var loop = new ExecutionContext(ctx.Type, ctx.Method) { This = ctx.This, Parent = ctx }; - loop.Set(Type.IndexLowercase, new ValueInstance(itemType.GetType(Base.Number), index)); - var value = iterator.GetIteratorValue(index); - if (itemType.Name == Base.Text && value is char character) - value = character.ToString(); - var valueInstance = value as ValueInstance ?? new ValueInstance(itemType, value); - loop.Set(Type.ValueLowercase, valueInstance); + var indexInstance = new ValueInstance(executor.numberType, index); + loop.Set(Type.IndexLowercase, indexInstance); + var value = iterator.IsPrimitiveType(executor.numberType) || + iterator.TryGetValueTypeInstance()?.ReturnType == executor.rangeType + ? indexInstance + : iterator.GetIteratorValue(itemType, index); + loop.Set(Type.ValueLowercase, value); foreach (var customVariable in f.CustomVariables) if (customVariable is VariableCall variableCall) - loop.Set(variableCall.Variable.Name, valueInstance); + loop.Set(variableCall.Variable.Name, value); var itemResult = f.Body is Body body ? EvaluateBody(body, loop) : executor.RunExpression(f.Body, loop); - if (itemResult.ReturnType.Name != Base.None && !itemResult.ReturnType.IsMutable) + if (loop.ExitMethodAndReturnValue.HasValue) + ctx.ExitMethodAndReturnValue = loop.ExitMethodAndReturnValue; + else if (!itemResult.IsPrimitiveType(executor.noneType) && !itemResult.IsMutable) + { + results ??= new List(); results.Add(itemResult); + } } private ValueInstance EvaluateBody(Body body, ExecutionContext ctx) { - var noneType = - (ctx.This?.ReturnType.Package ?? body.Method.Type.Package).FindType(Base.None)!; - ValueInstance last = new(noneType, null); + var last = executor.noneInstance; foreach (var e in body.Expressions) + { last = executor.RunExpression(e, ctx); + if (ctx.ExitMethodAndReturnValue.HasValue) + return ctx.ExitMethodAndReturnValue.Value; + } return last; } - private static ValueInstance? ShouldConsolidateForResult(List results, + private ValueInstance? ShouldConsolidateForResult(List? results, ExecutionContext ctx) { - if (ctx.Method.ReturnType.Name == Base.Number) - return new ValueInstance(ctx.Method.ReturnType, - results.Sum(value => EqualsExtensions.NumberToDouble(value.Value))); - if (ctx.Method.ReturnType.Name == Base.Text) + if (ctx.Method.ReturnType.IsNumber) { - var text = ""; - foreach (var value in results) - text += value.ReturnType.Name switch - { - Base.Number => (int)EqualsExtensions.NumberToDouble(value.Value), - Base.Character => "" + (char)value.Value!, - Base.Text => (string)value.Value!, - _ => throw new NotSupportedException("Can't append to text: " + value) - }; - return new ValueInstance(ctx.Method.ReturnType, text); + var sum = 0.0; + if (results != null) + for (var i = 0; i < results.Count; i++) + sum += results[i].Number; + return new ValueInstance(executor.numberType, sum); } - return ctx.Method.ReturnType.Name == Base.Boolean - ? new ValueInstance(ctx.Method.ReturnType, results.Any(value => value.Value is true)) - : null; + if (ctx.Method.ReturnType.IsBoolean) + { + var any = false; + if (results != null) + for (var i = 0; i < results.Count; i++) + if (results[i].Boolean) + { + any = true; + break; + } + return new ValueInstance(executor.booleanType, any); + } + if (!ctx.Method.ReturnType.IsText) + return null; + if (results == null) + return new ValueInstance(""); + var text = new StringBuilder(); + foreach (var value in results) + if (value.IsPrimitiveType(executor.characterType)) + text.Append((char)value.Number); + else if (value.IsText) + text.Append(value.Text); + else if (value.IsPrimitiveType(executor.numberType)) + text.Append(value.GetCachedNumberString()); + else if (value.IsPrimitiveType(executor.booleanType)) + text.Append(value.Boolean //ncrunch: no coverage + ? "true" + : "false"); + else if (value.IsList || value.IsDictionary) + { + if (text.Length > 0) + text.Append(", "); + text.Append('('); + text.Append(value.ToExpressionCodeString()); + text.Append(')'); + } + else + throw new NotSupportedException("For text return type cannot consolidate value " + value); + return new ValueInstance(text.ToString()); } - private static Type GetForValueType(ValueInstance iterator) => - iterator.ReturnType is GenericTypeImplementation { Generic.Name: Base.List } list - ? list.ImplementationTypes[0] - : iterator.ReturnType.Name == Base.Text - ? iterator.ReturnType.GetType(Base.Text) - : iterator.ReturnType.GetType(Base.Number); + private Type GetForValueType(ValueInstance iterator) => + iterator.IsText + ? executor.characterType + : iterator.IsList + ? iterator.GetIteratorType() + : executor.numberType; } \ No newline at end of file diff --git a/Strict.HighLevelRuntime/IfEvaluator.cs b/Strict.HighLevelRuntime/IfEvaluator.cs index c5851108..81ff43e5 100644 --- a/Strict.HighLevelRuntime/IfEvaluator.cs +++ b/Strict.HighLevelRuntime/IfEvaluator.cs @@ -7,18 +7,19 @@ internal sealed class IfEvaluator(Executor executor) { public ValueInstance Evaluate(If iff, ExecutionContext ctx) { - if (Executor.ToBool(executor.RunExpression(iff.Condition, ctx))) + executor.Statistics.IfCount++; + if (executor.RunExpression(iff.Condition, ctx).Boolean) { var thenResult = executor.RunExpression(iff.Then, ctx); return iff.Then is MutableReassignment || IsMutableInstanceCall(iff.Then) - ? new ValueInstance(iff.ReturnType.GetType(Base.None), null) + ? executor.noneInstance : thenResult; } if (iff.OptionalElse == null) - return new ValueInstance(iff.ReturnType.GetType(Base.None), null); + return executor.noneInstance; var elseResult = executor.RunExpression(iff.OptionalElse, ctx); return iff.OptionalElse is MutableReassignment || IsMutableInstanceCall(iff.OptionalElse) - ? new ValueInstance(iff.ReturnType.GetType(Base.None), null) + ? executor.noneInstance : elseResult; } diff --git a/Strict.HighLevelRuntime/MethodCallEvaluator.cs b/Strict.HighLevelRuntime/MethodCallEvaluator.cs index a8b40470..2f329f6c 100644 --- a/Strict.HighLevelRuntime/MethodCallEvaluator.cs +++ b/Strict.HighLevelRuntime/MethodCallEvaluator.cs @@ -1,7 +1,5 @@ using Strict.Expressions; using Strict.Language; -using System.Collections; -using System.Globalization; using Type = Strict.Language.Type; namespace Strict.HighLevelRuntime; @@ -10,17 +8,17 @@ public sealed class MethodCallEvaluator(Executor executor) { public ValueInstance EvaluateListCall(ListCall call, ExecutionContext ctx) { + executor.Statistics.ListCallCount++; var listInstance = executor.RunExpression(call.List, ctx); var indexValue = executor.RunExpression(call.Index, ctx); - var index = Convert.ToInt32(EqualsExtensions.NumberToDouble(indexValue.Value)); - if (listInstance.Value is IList list) - return list[index] as ValueInstance ?? new ValueInstance(call.ReturnType, list[index]); - throw new InvalidOperationException("List call can only be used on iterators, got: " + //ncrunch: no coverage - listInstance); + return listInstance.IsList + ? listInstance.GetIteratorValue(executor.characterType, (int)indexValue.Number) + : throw new InvalidOperationException("List call needs a list, got: " + listInstance); } public ValueInstance Evaluate(MethodCall call, ExecutionContext ctx) { + executor.Statistics.MethodCallCount++; var op = call.Method.Name; if (IsArithmetic(op) || IsCompare(op) || IsLogical(op)) return EvaluateArithmeticOrCompareOrLogical(call, ctx); @@ -46,6 +44,7 @@ private static bool IsLogical(string name) => private ValueInstance EvaluateArithmeticOrCompareOrLogical(MethodCall call, ExecutionContext ctx) { + executor.Statistics.BinaryCount++; if (call.Instance == null || call.Arguments.Count != 1) throw new InvalidOperationException("Binary call must have instance and 1 argument"); //ncrunch: no coverage var leftInstance = executor.RunExpression(call.Instance, ctx); @@ -54,164 +53,142 @@ private ValueInstance EvaluateArithmeticOrCompareOrLogical(MethodCall call, ? ExecuteArithmeticOperation(call, ctx, leftInstance, rightInstance) : IsCompare(call.Method.Name) ? ExecuteComparisonOperation(call, ctx, leftInstance, rightInstance) - : ExecuteBinaryOperation(call, ctx, leftInstance, rightInstance); + : ExecuteLogicalBinaryOperation(call, ctx, leftInstance, rightInstance); } private ValueInstance ExecuteArithmeticOperation(MethodCall call, ExecutionContext ctx, - ValueInstance leftInstance, ValueInstance rightInstance) + ValueInstance left, ValueInstance right) { + executor.Statistics.ArithmeticCount++; var op = call.Method.Name; - var left = leftInstance.Value; - var right = rightInstance.Value; - if (leftInstance.ReturnType.Name == Base.Number && - rightInstance.ReturnType.Name == Base.Number) + if (IsNumberLike(left) && IsNumberLike(right)) { - var l = EqualsExtensions.NumberToDouble(left); - var r = EqualsExtensions.NumberToDouble(right); + var l = left.Number; + var r = right.Number; return op switch { - BinaryOperator.Plus => Number(call.Method, l + r), - BinaryOperator.Minus => Number(call.Method, l - r), - BinaryOperator.Multiply => Number(call.Method, l * r), - BinaryOperator.Divide => Number(call.Method, l / r), - BinaryOperator.Modulate => Number(call.Method, l % r), - BinaryOperator.Power => Number(call.Method, Math.Pow(l, r)), - _ => ExecuteMethodCall(call, leftInstance, ctx) //ncrunch: no coverage + BinaryOperator.Plus => new ValueInstance(executor.numberType, l + r), + BinaryOperator.Minus => new ValueInstance(executor.numberType, l - r), + BinaryOperator.Multiply => new ValueInstance(executor.numberType, l * r), + BinaryOperator.Divide => new ValueInstance(executor.numberType, l / r), + BinaryOperator.Modulate => new ValueInstance(executor.numberType, l % r), + BinaryOperator.Power => new ValueInstance(executor.numberType, Math.Pow(l, r)), + _ => ExecuteMethodCall(call, left, ctx) //ncrunch: no coverage }; } - if (leftInstance.ReturnType.Name == Base.Text && rightInstance.ReturnType.Name == Base.Text) - { + if (left.IsText && right.IsText) return op == BinaryOperator.Plus - ? new ValueInstance(leftInstance.ReturnType, (string)left! + (string)right!) + ? new ValueInstance(left.Text + right.Text) : throw new NotSupportedException("Only + operator is supported for Text, got: " + op); - } - if (leftInstance.ReturnType.Name == Base.Text && rightInstance.ReturnType.Name == Base.Number) + if (left.IsText && right.IsPrimitiveType(executor.numberType)) { return op == BinaryOperator.Plus - ? new ValueInstance(leftInstance.ReturnType, - (string)left! + (int)EqualsExtensions.NumberToDouble(right)) - : throw new NotSupportedException("Only + operator is supported for Text+Number, got: " + op); + ? new ValueInstance(left.Text + right.Number) + : throw new NotSupportedException("Only + operator is supported for Text+Number, got: " + + op); } - if (leftInstance.ReturnType.IsIterator && rightInstance.ReturnType.IsIterator) + if (left.IsList && right.IsList) { - if (left is not IList leftList || - right is not IList rightList) - throw new InvalidOperationException( //ncrunch: no coverage - "Expected List for iterator operation, " + - "other iterators are not yet supported: left=" + left + ", right=" + right); if (op is BinaryOperator.Multiply or BinaryOperator.Divide && - leftList.Count != rightList.Count) + left.List.Items.Count != right.List.Items.Count) return Error(ListsHaveDifferentDimensions, ctx, call); return op switch { - BinaryOperator.Plus => CombineLists(leftInstance.ReturnType, leftList, rightList), - BinaryOperator.Minus => SubtractLists(leftInstance.ReturnType, leftList, rightList), - BinaryOperator.Multiply => MultiplyLists(leftInstance.ReturnType, leftList, rightList), - BinaryOperator.Divide => DivideLists(leftInstance.ReturnType, leftList, rightList), + BinaryOperator.Plus => CombineLists(left.List.ReturnType, left.List.Items, + right.List.Items), + BinaryOperator.Minus => SubtractLists(left.List.ReturnType, left.List.Items, + right.List.Items), + BinaryOperator.Multiply => MultiplyLists(left.List.ReturnType, executor.numberType, + left.List.Items, right.List.Items), + BinaryOperator.Divide => DivideLists(left.List.ReturnType, executor.numberType, + left.List.Items, right.List.Items), _ => throw new NotSupportedException( //ncrunch: no coverage "Only +, -, *, / operators are supported for Lists, got: " + op) }; } - if (leftInstance.ReturnType.IsIterator && rightInstance.ReturnType.Name == Base.Number) + if (left.IsList && right.IsPrimitiveType(executor.numberType)) { - if (left is not IList leftList) - throw new InvalidOperationException("Expected left list for iterator operation " + //ncrunch: no coverage - op + ": left=" + left + ", right=" + right); if (op == BinaryOperator.Plus) - return AddToList(leftInstance.ReturnType, leftList, rightInstance); + return AddToList(left.List.ReturnType, left.List.Items, right); if (op == BinaryOperator.Minus) - return RemoveFromList(leftInstance.ReturnType, leftList, rightInstance); - if (right is not double rightNumber) - throw new InvalidOperationException("Expected right number for iterator operation " + //ncrunch: no coverage - op + ": left=" + left + ", right=" + right); + return RemoveFromList(left.List.ReturnType, left.List.Items, right); if (op == BinaryOperator.Multiply) - return MultiplyList(leftInstance.ReturnType, leftList, rightNumber); + return MultiplyList(left.List.ReturnType, left.List.Items, right.Number); if (op == BinaryOperator.Divide) - return DivideList(leftInstance.ReturnType, leftList, rightNumber); + return DivideList(left.List.ReturnType, left.List.Items, right.Number); throw new NotSupportedException( //ncrunch: no coverage "Only +, -, *, / operators are supported for List and Number, got: " + op); } - return ExecuteMethodCall(call, leftInstance, ctx); //ncrunch: no coverage + return ExecuteMethodCall(call, left, ctx); //ncrunch: no coverage } - private static ValueInstance Number(Context any, double n) => new(any.GetType(Base.Number), n); + private bool IsNumberLike(ValueInstance value) => value.IsNumberLike(executor.numberType); public const string ListsHaveDifferentDimensions = "listsHaveDifferentDimensions"; private ValueInstance ExecuteComparisonOperation(MethodCall call, ExecutionContext ctx, - ValueInstance leftInstance, ValueInstance rightInstance) + ValueInstance left, ValueInstance right) { + executor.Statistics.CompareCount++; var op = call.Method.Name; - var left = leftInstance.Value!; - var right = rightInstance.Value!; - if (op is BinaryOperator.Is or UnaryOperator.Not) + if (op is BinaryOperator.Is) { - if (rightInstance.ReturnType.IsError) - { - var matches = rightInstance.ReturnType.Name == Base.Error - ? leftInstance.ReturnType.IsError - : leftInstance.ReturnType.IsSameOrCanBeUsedAs(rightInstance.ReturnType); - return op is BinaryOperator.Is - ? Executor.Bool(call.Method, matches) - : Executor.Bool(call.Method, !matches); - } - if (leftInstance.ReturnType.Name == Base.Character && right is string rightText) - { - right = (int)rightText[0]; - rightInstance = new ValueInstance(leftInstance.ReturnType, right); - } - if (leftInstance.ReturnType.Name == Base.Text && right is int rightInt) + var rightInstance = right.TryGetValueTypeInstance(); + if (rightInstance is { ReturnType.IsError: true }) { - right = rightInt + ""; - rightInstance = new ValueInstance(leftInstance.ReturnType, right); + var leftInstance = left.TryGetValueTypeInstance(); + var matches = leftInstance != null && leftInstance.ReturnType.IsError && + leftInstance.ReturnType.IsSameOrCanBeUsedAs(rightInstance.ReturnType); + return executor.ToBoolean(matches); } - var equals = leftInstance.Equals(rightInstance); - return Executor.Bool(call.Method, op is BinaryOperator.Is - ? equals - : !equals); + if (left.IsPrimitiveType(executor.characterType) && right.IsText) + right = new ValueInstance(executor.characterType, right.Text[0]); + if (left.IsText && + (right.IsPrimitiveType(executor.numberType) || right.IsPrimitiveType(executor.characterType))) + right = new ValueInstance(right.ToExpressionCodeString()); + return executor.ToBoolean(left.Equals(right)); } - var l = EqualsExtensions.NumberToDouble(left); - var r = EqualsExtensions.NumberToDouble(right); + var l = left.Number; + var r = right.Number; return op switch { - BinaryOperator.Greater => Executor.Bool(call.Method, l > r), - BinaryOperator.Smaller => Executor.Bool(call.Method, l < r), - BinaryOperator.GreaterOrEqual => Executor.Bool(call.Method, l >= r), - BinaryOperator.SmallerOrEqual => Executor.Bool(call.Method, l <= r), - _ => ExecuteMethodCall(call, leftInstance, ctx) //ncrunch: no coverage + BinaryOperator.Greater => executor.ToBoolean(l > r), + BinaryOperator.Smaller => executor.ToBoolean(l < r), + BinaryOperator.GreaterOrEqual => executor.ToBoolean(l >= r), + BinaryOperator.SmallerOrEqual => executor.ToBoolean(l <= r), + _ => ExecuteMethodCall(call, left, ctx) //ncrunch: no coverage }; } - private ValueInstance ExecuteBinaryOperation(MethodCall call, ExecutionContext ctx, - ValueInstance leftInstance, ValueInstance rightInstance) + private ValueInstance ExecuteLogicalBinaryOperation(MethodCall call, ExecutionContext ctx, + ValueInstance left, ValueInstance right) { - var left = leftInstance.Value; - var right = rightInstance.Value; + executor.Statistics.LogicalOperationCount++; return call.Method.Name switch { - BinaryOperator.And => Executor.Bool(call.Method, Executor.ToBool(left) && Executor.ToBool(right)), - BinaryOperator.Or => Executor.Bool(call.Method, Executor.ToBool(left) || Executor.ToBool(right)), - BinaryOperator.Xor => Executor.Bool(call.Method, Executor.ToBool(left) ^ Executor.ToBool(right)), - _ => ExecuteMethodCall(call, leftInstance, ctx) //ncrunch: no coverage + BinaryOperator.And => executor.ToBoolean(left.Boolean && right.Boolean), + BinaryOperator.Or => executor.ToBoolean(left.Boolean || right.Boolean), + BinaryOperator.Xor => executor.ToBoolean(left.Boolean ^ right.Boolean), + _ => ExecuteMethodCall(call, left, ctx) //ncrunch: no coverage }; } - private static ValueInstance CombineLists(Type listType, ICollection leftList, - ICollection rightList) + private static ValueInstance CombineLists(Type listType, IReadOnlyList leftList, + IReadOnlyList rightList) { var combined = new List(leftList.Count + rightList.Count); - var isLeftText = listType is GenericTypeImplementation { Generic.Name: Base.List } list && - list.ImplementationTypes[0].Name == Base.Text; + var isLeftText = listType is GenericTypeImplementation { Generic.Name: Type.List } list && + list.ImplementationTypes[0].IsText; foreach (var item in leftList) combined.Add(item); foreach (var item in rightList) - combined.Add(isLeftText && item.ReturnType.Name != Base.Text - ? new ValueInstance(listType.GetType(Base.Text), item.Value?.ToString()) + combined.Add(isLeftText && !item.IsText + ? new ValueInstance(item.ToExpressionCodeString()) : item); return new ValueInstance(listType, combined); } - private static ValueInstance SubtractLists(Type listType, IEnumerable leftList, - IEnumerable rightList) + private static ValueInstance SubtractLists(Type listType, IReadOnlyList leftList, + IReadOnlyList rightList) { var remainder = new List(); foreach (var item in leftList) @@ -221,53 +198,40 @@ private static ValueInstance SubtractLists(Type listType, IEnumerable leftList, - IList rightList) + private static ValueInstance MultiplyLists(Type leftListType, Type numberType, + IReadOnlyList leftList, IReadOnlyList rightList) { var result = new List(); for (var index = 0; index < leftList.Count; index++) - result.Add(new ValueInstance(leftListType.GetType(Base.Number), - EqualsExtensions.NumberToDouble(leftList[index].Value) * - EqualsExtensions.NumberToDouble(rightList[index].Value))); + result.Add(new ValueInstance(numberType, leftList[index].Number * rightList[index].Number)); return new ValueInstance(leftListType, result); } - private static ValueInstance DivideLists(Type leftListType, IList leftList, - IList rightList) + private static ValueInstance DivideLists(Type leftListType, Type numberType, + IReadOnlyList leftList, IReadOnlyList rightList) { var result = new List(); for (var index = 0; index < leftList.Count; index++) - result.Add(new ValueInstance(leftListType.GetType(Base.Number), - EqualsExtensions.NumberToDouble(leftList[index].Value) / - EqualsExtensions.NumberToDouble(rightList[index].Value))); + result.Add(new ValueInstance(numberType, leftList[index].Number / rightList[index].Number)); return new ValueInstance(leftListType, result); } - private static ValueInstance AddToList(Type leftListType, ICollection leftList, + private static ValueInstance AddToList(Type leftListType, IReadOnlyList leftList, ValueInstance right) { var combined = new List(leftList.Count + 1); - var isLeftText = leftListType is GenericTypeImplementation { Generic.Name: Base.List } list && - list.ImplementationTypes[0].Name == Base.Text; + var isLeftText = leftListType is GenericTypeImplementation { Generic.Name: Type.List } list && + list.ImplementationTypes[0].IsText; foreach (var item in leftList) combined.Add(item); - combined.Add(isLeftText && right.ReturnType.Name != Base.Text - ? new ValueInstance(leftListType.GetType(Base.Text), ConvertToText(right.Value)) + combined.Add(isLeftText && !right.IsText + ? new ValueInstance(right.ToExpressionCodeString()) : right); return new ValueInstance(leftListType, combined); } - private static string ConvertToText(object? value) => - value switch - { - string text => text, //ncrunch: no coverage - double number => number.ToString(CultureInfo.InvariantCulture), - int number => number.ToString(CultureInfo.InvariantCulture), //ncrunch: no coverage - _ => value?.ToString() ?? string.Empty //ncrunch: no coverage - }; - private static ValueInstance RemoveFromList(Type leftListType, - IEnumerable leftList, ValueInstance right) + IReadOnlyList leftList, ValueInstance right) { var result = new List(); foreach (var item in leftList) @@ -277,101 +241,105 @@ private static ValueInstance RemoveFromList(Type leftListType, } private static ValueInstance MultiplyList(Type leftListType, - ICollection leftList, double rightNumber) + IReadOnlyList leftList, double rightNumber) { var result = new List(leftList.Count); foreach (var item in leftList) - result.Add(new ValueInstance(item.ReturnType, - EqualsExtensions.NumberToDouble(item.Value) * rightNumber)); + result.Add(new ValueInstance(item.GetTypeExceptText(), item.Number * rightNumber)); return new ValueInstance(leftListType, result); } - private static ValueInstance DivideList(Type leftListType, ICollection leftList, + private static ValueInstance DivideList(Type leftListType, IReadOnlyList leftList, double rightNumber) { - var result = new List(leftList.Count); + var result = new List(leftList.Count); foreach (var item in leftList) - result.Add(new ValueInstance(item.ReturnType, - EqualsExtensions.NumberToDouble(item.Value) / rightNumber)); + result.Add(new ValueInstance(item.GetTypeExceptText(), item.Number / rightNumber)); return new ValueInstance(leftListType, result); } private ValueInstance ExecuteMethodCall(MethodCall call, ValueInstance? instance, ExecutionContext ctx) { - var args = new List(call.Arguments.Count); - foreach (var a in call.Arguments) - args.Add(executor.RunExpression(a, ctx)); - if (instance is { ReturnType.IsDictionary: true } && args.Count > 0 && call.Method.Name == "Add") + IReadOnlyList args; + if (call.Arguments.Count == 0) + args = []; + else + { + var argsArray = new ValueInstance[call.Arguments.Count]; + for (var i = 0; i < call.Arguments.Count; i++) + argsArray[i] = executor.RunExpression(call.Arguments[i], ctx); + args = argsArray; + } + if (instance is { IsDictionary: true } && args.Count > 0 && call.Method.Name == "Add") { if (args.Count == 2) - ((IDictionary)instance.Value!)[args[0]] = args[1]; - return instance; + instance.Value.GetDictionaryItems()[args[0]] = args[1]; + return instance.Value; } - var result = executor.Execute(call.Method, instance, args, ctx); + var result = executor.Execute(call.Method, instance ?? executor.noneInstance, args, ctx); if (call.Method.ReturnType.IsMutable && call.Instance is VariableCall variableCall && - instance != null) - ctx.Set(variableCall.Variable.Name, new ValueInstance(instance.ReturnType, result.Value)); + !instance.Equals(executor.noneInstance)) + ctx.Set(variableCall.Variable.Name, result); return result; } - private static ValueInstance Error(string name, ExecutionContext ctx, Expression? source = null) + private ValueInstance Error(string name, ExecutionContext ctx, Expression? source = null) { - var stacktraceList = new List { CreateStacktrace(ctx, source) }; - var errorMembers = new Dictionary(StringComparer.OrdinalIgnoreCase); - var errorType = ctx.Method.GetType(Base.Error); + var errorMembers = new Dictionary(StringComparer.OrdinalIgnoreCase); + var errorType = ctx.Method.GetType(Type.Error); foreach (var member in errorType.Members) errorMembers[member.Name] = member.Type.Name switch { - Base.Name or Base.Text => name, - _ when member.Type.Name == Base.List || member.Type is GenericTypeImplementation - { - Generic.Name: Base.List - } => stacktraceList, + nameof(Type.Name) or Type.Text => new ValueInstance(name), + _ when member.Type.IsList => CreateStacktrace(ctx, source), _ => throw new NotSupportedException("Error member not supported: " + member) //ncrunch: no coverage }; return new ValueInstance(errorType, errorMembers); } - private static Dictionary CreateStacktrace(ExecutionContext ctx, - Expression? source) + private ValueInstance CreateStacktrace(ExecutionContext ctx, Expression? source) { - var members = new Dictionary(StringComparer.OrdinalIgnoreCase); - var stacktraceType = ctx.Method.GetType(Base.Stacktrace); + var members = new Dictionary(StringComparer.OrdinalIgnoreCase); + var stacktraceType = ctx.Method.GetType(Type.Stacktrace); foreach (var member in stacktraceType.Members) members[member.Name] = member.Type.Name switch { - Base.Method => CreateMethodValue(ctx.Method), - Base.Text or Base.Name => ctx.Method.Type.FilePath, - Base.Number => (double)(source?.LineNumber ?? ctx.Method.TypeLineNumber), + nameof(Method) => new ValueInstance(ctx.Method.GetType(nameof(Method)), + CreateMethodValue(ctx.Method)), + Type.Text or nameof(Type.Name) => new ValueInstance(ctx.Method.Type.FilePath), + Type.Number => new ValueInstance(executor.numberType, + source?.LineNumber ?? ctx.Method.TypeLineNumber), _ => throw new NotSupportedException("Stacktrace member not supported: " + member) //ncrunch: no coverage }; - return members; + return new ValueInstance(executor.listType.GetGenericImplementation(stacktraceType), + [new ValueInstance(stacktraceType, members)]); } - private static Dictionary CreateMethodValue(Method method) + private static Dictionary CreateMethodValue(Method method) { - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - var methodType = method.GetType(Base.Method); + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + var methodType = method.GetType(nameof(Method)); foreach (var member in methodType.Members) values[member.Name] = member.Type.Name switch { - Base.Name or Base.Text => method.Name, - Base.Type => CreateTypeValue(method.Type), + nameof(Type.Name) or Type.Text => new ValueInstance(method.Name), + nameof(Type) => new ValueInstance(method.GetType(nameof(Type)), + CreateTypeValue(method.Type)), _ => throw new NotSupportedException("Method member not supported: " + member) //ncrunch: no coverage }; return values; } - private static Dictionary CreateTypeValue(Type type) + private static Dictionary CreateTypeValue(Type type) { - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - var typeType = type.GetType(Base.Type); + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + var typeType = type.GetType(nameof(Type)); foreach (var member in typeType.Members) values[member.Name] = member.Type.Name switch { - Base.Name => type.Name, - Base.Text => type.Package.FullName, + nameof(Type.Name) => new ValueInstance(type.Name), + Type.Text => new ValueInstance(type.Package.FullName), _ => throw new NotSupportedException("Type member not supported: " + member) //ncrunch: no coverage }; return values; diff --git a/Strict.HighLevelRuntime/SelectorIfEvaluator.cs b/Strict.HighLevelRuntime/SelectorIfEvaluator.cs index 6edfa0aa..e066d254 100644 --- a/Strict.HighLevelRuntime/SelectorIfEvaluator.cs +++ b/Strict.HighLevelRuntime/SelectorIfEvaluator.cs @@ -1,5 +1,4 @@ using Strict.Expressions; -using Strict.Language; namespace Strict.HighLevelRuntime; @@ -7,11 +6,12 @@ internal sealed class SelectorIfEvaluator(Executor executor) { public ValueInstance Evaluate(SelectorIf selectorIf, ExecutionContext ctx) { + executor.Statistics.SelectorIfCount++; foreach (var @case in selectorIf.Cases) - if (Executor.ToBool(executor.RunExpression(@case.Condition, ctx))) + if (executor.RunExpression(@case.Condition, ctx).Boolean) return executor.RunExpression(@case.Then, ctx); return selectorIf.OptionalElse != null ? executor.RunExpression(selectorIf.OptionalElse, ctx) - : new ValueInstance(selectorIf.ReturnType.GetType(Base.None), null); + : executor.noneInstance; } -} +} \ No newline at end of file diff --git a/Strict.HighLevelRuntime/Statistics.cs b/Strict.HighLevelRuntime/Statistics.cs new file mode 100644 index 00000000..6460d41f --- /dev/null +++ b/Strict.HighLevelRuntime/Statistics.cs @@ -0,0 +1,82 @@ +using Strict.Expressions; +using Strict.Language; + +namespace Strict.HighLevelRuntime; + +/// +/// Keeps track of things happening the classes here, usually used together with TestRunner. +/// +//ncrunch: no coverage start +public sealed record Statistics +{ + public int PackagesTested { get; internal set; } + public int TypesTested { get; internal set; } + public int MethodsTested { get; internal set; } + public int TestExpressions { get; internal set; } + public int MethodCount { get; internal set; } + public int ExpressionCount { get; internal set; } + public int BodyCount { get; internal set; } + public int IfCount { get; internal set; } + public int SelectorIfCount { get; internal set; } + public int ForCount { get; internal set; } + public int MethodCallCount { get; internal set; } + public int MemberCallCount { get; internal set; } + public int ListCallCount { get; internal set; } + public int BinaryCount { get; internal set; } + public int ArithmeticCount { get; internal set; } + public int CompareCount { get; internal set; } + public int LogicalOperationCount { get; internal set; } + public int UnaryCount { get; internal set; } + public int FromCreationsCount { get; internal set; } + public int ToConversionCount { get; internal set; } + public int ReturnCount { get; internal set; } + public int VariableDeclarationCount { get; internal set; } + public int VariableCallCount { get; internal set; } + public int MutableDeclarationCount { get; internal set; } + public int MutableUsageCount { get; internal set; } + public int FindVariableCount { get; internal set; } + public int FindTypeCount => Context.FindTypeCount; + public int GetPrimitiveCodeStringCalls => ValueInstance.GetPrimitiveCodeStringCalls; + public int GetPrimitiveCodeStringCallsNonNumberBooleanChar => ValueInstance.GetPrimitiveCodeStringCallsNonNumberBooleanChar; + public int EqualsCalls => ValueInstance.EqualsCalls; + public int ToExpressionCodeStringCalls => ValueInstance.ToExpressionCodeStringCalls; + public int ToExpressionCodeStringEscapedCalls => ValueInstance.ToExpressionCodeStringEscapedCalls; + public int ToExpressionCodeStringTypeIdCalls => ValueInstance.ToExpressionCodeStringTypeIdCalls; + + public void Reset() + { + PackagesTested = 0; + TypesTested = 0; + MethodsTested = 0; + TestExpressions = 0; + MethodCount = 0; + ExpressionCount = 0; + BodyCount = 0; + IfCount = 0; + SelectorIfCount = 0; + ForCount = 0; + MethodCallCount = 0; + MemberCallCount = 0; + ListCallCount = 0; + BinaryCount = 0; + ArithmeticCount = 0; + CompareCount = 0; + LogicalOperationCount = 0; + UnaryCount = 0; + FromCreationsCount = 0; + ToConversionCount = 0; + ReturnCount = 0; + VariableDeclarationCount = 0; + VariableCallCount = 0; + MutableDeclarationCount = 0; + MutableUsageCount = 0; + FindVariableCount = 0; + Context.FindTypeCount = 0; + ValueInstance.GetPrimitiveCodeStringCalls = 0; + ValueInstance.GetPrimitiveCodeStringCallsNonNumberBooleanChar = 0; + ValueInstance.EqualsCalls = 0; + ValueInstance.ToExpressionCodeStringCalls = 0; + ValueInstance.ToExpressionCodeStringEscapedCalls = 0; + ValueInstance.ToExpressionCodeStringTypeIdCalls = 0; + } +} \ No newline at end of file diff --git a/Strict.HighLevelRuntime/ToEvaluator.cs b/Strict.HighLevelRuntime/ToEvaluator.cs index 4411516f..3f0bf01c 100644 --- a/Strict.HighLevelRuntime/ToEvaluator.cs +++ b/Strict.HighLevelRuntime/ToEvaluator.cs @@ -1,6 +1,6 @@ using Strict.Expressions; -using Strict.Language; using System.Globalization; +using Type = Strict.Language.Type; namespace Strict.HighLevelRuntime; @@ -8,17 +8,20 @@ internal sealed class ToEvaluator(Executor executor) { public ValueInstance Evaluate(To to, ExecutionContext ctx) { - var left = executor.RunExpression(to.Instance!, ctx).Value; - if (to.Instance!.ReturnType.Name == Base.Text && to.ConversionType.Name == Base.Number && - left is string textValue) + executor.Statistics.ToConversionCount++; + var left = executor.RunExpression(to.Instance!, ctx); + if (to.Instance!.ReturnType.IsText && to.ConversionType.IsNumber && left.IsText) return new ValueInstance(to.ConversionType, - double.Parse(textValue, CultureInfo.InvariantCulture)); - if (to.ConversionType.Name == Base.Text) - return new ValueInstance(to.ConversionType, left?.ToString() ?? ""); - if (!to.Method.IsTrait && to.Method.Type.Name != Base.Number) - return executor.EvaluateMethodCall(to, ctx); - return !to.Method.IsTrait - ? executor.EvaluateMethodCall(to, ctx) - : throw new NotSupportedException("Conversion to " + to.ConversionType.Name + " not supported"); + double.Parse(left.Text, CultureInfo.InvariantCulture)); + if (to.ConversionType.IsText) + return new ValueInstance(left.ToExpressionCodeString()); + return to.Method.IsTrait + ? throw new ToMethodNotImplemented(left, to.ConversionType) + : executor.EvaluateMethodCall(to, ctx); } -} + + //ncrunch: no coverage start + public sealed class ToMethodNotImplemented(ValueInstance left, Type toConversionType) + : ExecutionFailed(toConversionType, "Conversion from " + left + " to " + + toConversionType.Name + " not supported"); +} \ No newline at end of file diff --git a/Strict.HighLevelRuntime/ValueInstance.cs b/Strict.HighLevelRuntime/ValueInstance.cs deleted file mode 100644 index 87c9a128..00000000 --- a/Strict.HighLevelRuntime/ValueInstance.cs +++ /dev/null @@ -1,150 +0,0 @@ -using Strict.Language; -using System.Collections; -using Type = Strict.Language.Type; - -namespace Strict.HighLevelRuntime; - -public sealed class ValueInstance : IEquatable -{ - public ValueInstance(Type returnType, object? value) - { - if (value is Expression) - throw new InvalidTypeValue(returnType, Value); //ncrunch: no coverage - ReturnType = returnType; - Value = CheckIfValueMatchesReturnType(ReturnType.IsMutable - ? ((GenericTypeImplementation)ReturnType).ImplementationTypes[0] - : ReturnType, value); - } - - public Type ReturnType { get; } - public object? Value { get; } - - private static object? CheckIfValueMatchesReturnType(Type type, object? value) - { - if (type.Name == Base.None) - { - if (value is not null) - throw new InvalidTypeValue(type, value); - } - else if (value is null) - throw new InvalidTypeValue(type, value); - else if (type.IsBoolean) - { - if (value is not bool) - throw new InvalidTypeValue(type, value); - } - else if (type.IsEnum) - { - if (value is not int && value is not string) - throw new InvalidTypeValue(type, value); - } - else if (type.Name is Base.Text or Base.Name) - { - if (value is not string) - throw new InvalidTypeValue(type, value); - } - else if (type.Name is Base.Character or Base.HashCode || type.Members.Count == 1 && - type.IsSameOrCanBeUsedAs(type.GetType(Base.Character))) - { - if (value is double doubleValue) - return (int)doubleValue; - if (value is not char && value is not int) - throw new InvalidTypeValue(type, value); - } - else if (type.Name == Base.List || type.Name == Base.Dictionary || - type is GenericTypeImplementation { Generic.Name: Base.List } || - type is GenericTypeImplementation { Generic.Name: Base.Dictionary } || - type is GenericType { Generic.Name: Base.List } || type is GenericType - { - Generic.Name: Base.Dictionary - }) - { - if (value is IList) - throw new InvalidTypeValue(type, value); - if (value is not IList and not IDictionary and not string) - throw new InvalidTypeValue(type, value); - } - else if (type.Name == Base.Number || type.Members.Count == 1 && - type.IsSameOrCanBeUsedAs(type.GetType(Base.Number))) - { - if (value is char charValue) - return (int)charValue; - if (value is not double && value is not int) - throw new InvalidTypeValue(type, value); - } - else if (value is IDictionary valueDictionary) - { - foreach (var assignMember in valueDictionary) - if (type.Members.All(m => - !m.Name.Equals(assignMember.Key, StringComparison.OrdinalIgnoreCase))) - throw new UnableToAssignMemberToType(assignMember, valueDictionary, type); - } - else if (!type.IsSameOrCanBeUsedAs(type.GetType(Base.Error))) - throw new InvalidTypeValue(type, value); - return value; - } - - public sealed class UnableToAssignMemberToType(KeyValuePair member, - IDictionary values, - Type returnType) : ExecutionFailed(returnType, - "Can't assign member " + member + " (of " + values.DictionaryToWordList() + ") to " + - returnType + " " + returnType.Members.ToBrackets()); - - public sealed class InvalidTypeValue(Type returnType, object? value) : ExecutionFailed(returnType, - value switch //ncrunch: no coverage - { - null => "null", - Expression => "Expression " + value + " needs to be evaluated!", //ncrunch: no coverage - IEnumerable valueEnumerable => valueEnumerable.EnumerableToWordList(", ", true), - _ => value + "" - } + " (" + value?.GetType() + ") for " + returnType.Name); - - public override string ToString() => - ReturnType.Name == Base.Boolean - ? $"{Value}" - : Value is IEnumerable valueEnumerable - ? $"{ReturnType.Name}: " + valueEnumerable.EnumerableToWordList(", ", true) - : ReturnType.IsIterator - ? $"Unknown Iterator {ReturnType.Name}: {Value}" - : $"{ReturnType.Name}:{Value}"; - - public bool Equals(ValueInstance? other) => - ReferenceEquals(this, other) || other is not null && - other.ReturnType.IsSameOrCanBeUsedAs(ReturnType) && - EqualsExtensions.AreEqual(Value, other.Value); - - public override bool Equals(object? obj) => - ReferenceEquals(this, obj) || obj is ValueInstance other && Equals(other); - - public override int GetHashCode() => HashCode.Combine(ReturnType, Value); - - public object? FindInnerValue(string name) - { - if (Value is IDictionary valueDictionary) - if (valueDictionary.TryGetValue(name, out var value)) - return value; - return null; //ncrunch: no coverage - } - - public Index GetIteratorLength() => - Value switch - { - IList list => list.Count, - int count => count, //ncrunch: no coverage, in Strict numbers are double - double countDouble => (int)countDouble, - string text => text.Length, - _ => throw new IteratorNotSupported(this) - }; - - public class IteratorNotSupported(ValueInstance instance) - : ExecutionFailed(instance.ReturnType, instance.ToString()); - - public object? GetIteratorValue(int index) => - ReturnType.Name is Base.Number or Base.Range - ? index - : Value is string - ? ((string)Value!)[index] - : Value is IList list - ? list[index] - : throw new IteratorNotSupported(this); -} \ No newline at end of file diff --git a/Strict.Language.Tests/EnumTests.cs b/Strict.Language.Tests/EnumTests.cs index 6fa63d6e..bbed020c 100644 --- a/Strict.Language.Tests/EnumTests.cs +++ b/Strict.Language.Tests/EnumTests.cs @@ -95,7 +95,7 @@ public void CompareEnums() "\t\treturn numbers(0) + numbers(1)")). ParseMembersAndMethods(parser); var ifExpression = (If)consumingType.Methods[0].GetBodyAndParseIfNeeded(); - Assert.That(ifExpression.Condition.ReturnType.Name, Is.EqualTo(Base.Boolean)); + Assert.That(ifExpression.Condition.ReturnType.IsBoolean, Is.True); var binary = (Binary)ifExpression.Condition; Assert.That(((MemberCall)binary.Arguments[0]).Member.Name, Is.EqualTo("Add")); Assert.That(((MemberCall)((MemberCall)binary.Instance!).Member.InitialValue!).Member.Name, diff --git a/Strict.Language.Tests/ExpressionParserTests.cs b/Strict.Language.Tests/ExpressionParserTests.cs index 555dfc03..0693eb58 100644 --- a/Strict.Language.Tests/ExpressionParserTests.cs +++ b/Strict.Language.Tests/ExpressionParserTests.cs @@ -1,10 +1,13 @@ +using Boolean = Strict.Expressions.Boolean; + namespace Strict.Language.Tests; public class ExpressionParserTests : ExpressionParser { [SetUp] public void CreateType() => - type = new Type(TestPackage.Instance, new MockRunTypeLines()).ParseMembersAndMethods(this); + type = new Type(TestPackage.Instance, new MockRunTypeLines(nameof(ExpressionParserTests))). + ParseMembersAndMethods(this); private Type type = null!; @@ -26,6 +29,10 @@ public void ParsingHappensAfterCallingGetBodyAndParseIfNeeded() public class TestExpression(Type returnType) : Expression(returnType) { public override bool IsConstant => false; //ncrunch: no coverage + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is TestExpression t && ReturnType == t.ReturnType); + public override int GetHashCode() => ReturnType.GetHashCode(); } public override Expression ParseLineExpression(Body body, ReadOnlySpan line) @@ -37,7 +44,7 @@ public override Expression ParseLineExpression(Body body, ReadOnlySpan lin //ncrunch: no coverage start, not the focus here public override Expression ParseExpression(Body body, ReadOnlySpan text, bool isMutable = false) => - new Value(type.GetType(Base.Boolean), int.TryParse(text, out _), 0, isMutable); + new Boolean(type, true); public override List ParseListArguments(Body body, ReadOnlySpan text) => null!; diff --git a/Strict.Language.Tests/GenericTypeImplementationTests.cs b/Strict.Language.Tests/GenericTypeImplementationTests.cs index 9b7a67bd..afece021 100644 --- a/Strict.Language.Tests/GenericTypeImplementationTests.cs +++ b/Strict.Language.Tests/GenericTypeImplementationTests.cs @@ -41,9 +41,9 @@ public void TypeArgumentsDoNotMatchGenericTypeConstructor() => }, //ncrunch: no coverage Throws.InstanceOf().With.InnerException. InstanceOf().With.Message.Contains( - "The generic type TestPackage.Comparer needs these type arguments: (Generic TestPackage." + - "Generic, SecondType TestPackage.Generic), this does not match provided types: (TestPackage." + - "Text)")); + "The generic type TestPackage/Comparer needs these type arguments: " + + "(Generic TestPackage/Generic, SecondType TestPackage/Generic), this does not match " + + "provided types: (TestPackage/Text)")); [Test] public void GenericTypeWithMultipleImplementations() @@ -99,12 +99,12 @@ public void GenericTypeFromConstructorInitializesAllMembers() Assert.That(methodCall.Arguments, Has.Count.EqualTo(2)); Assert.That(methodCall.Method.Type, Is.InstanceOf()); var customErrorType = (GenericTypeImplementation)methodCall.Method.Type; - Assert.That(customErrorType.Generic.Name, Is.EqualTo(Base.ErrorWithValue)); + Assert.That(customErrorType.Generic.Name, Is.EqualTo(Type.ErrorWithValue)); Assert.That(customErrorType.Members, Has.Count.EqualTo(2)); Assert.That(customErrorType.Members[0].Name, Is.EqualTo("Error")); - Assert.That(customErrorType.Members[0].Type.Name, Is.EqualTo(Base.Error)); + Assert.That(customErrorType.Members[0].Type.Name, Is.EqualTo(Type.Error)); Assert.That(customErrorType.Members[1].Name, Is.EqualTo("Value")); - Assert.That(customErrorType.Members[1].Type.Name, Is.EqualTo(Base.Number)); + Assert.That(customErrorType.Members[1].Type.Name, Is.EqualTo(Type.Number)); Assert.That(customErrorType.AvailableMethods, Has.Count.GreaterThan(1)); Assert.That(customErrorType.AvailableMethods[Method.From][0].Parameters, Has.Count.EqualTo(2), customErrorType.ToString()); @@ -113,11 +113,11 @@ public void GenericTypeFromConstructorInitializesAllMembers() [Test] public void DictionaryImplementationUsesListMemberType() { - var number = TestPackage.Instance.GetType(Base.Number); - var dictionary = TestPackage.Instance.GetType(Base.Dictionary). + var number = TestPackage.Instance.GetType(Type.Number); + var dictionary = TestPackage.Instance.GetType(Type.Dictionary). GetGenericImplementation(number, number); var listMember = dictionary.Members[0].Type; Assert.That(listMember, Is.InstanceOf()); - Assert.That(((GenericTypeImplementation)listMember).Generic.Name, Is.EqualTo(Base.List)); + Assert.That(((GenericTypeImplementation)listMember).Generic.Name, Is.EqualTo(Type.List)); } } \ No newline at end of file diff --git a/Strict.Language.Tests/MethodTests.cs b/Strict.Language.Tests/MethodTests.cs index df04d996..04008da5 100644 --- a/Strict.Language.Tests/MethodTests.cs +++ b/Strict.Language.Tests/MethodTests.cs @@ -5,7 +5,7 @@ public sealed class MethodTests [SetUp] public void CreateType() { - type = new Type(TestPackage.Instance, new MockRunTypeLines()); + type = new Type(TestPackage.Instance, new MockRunTypeLines(nameof(MethodTests))); parser = new MethodExpressionParser(); } @@ -49,7 +49,7 @@ public void ParseDefinition() var method = new Method(type, 0, null!, [Run]); Assert.That(method.Name, Is.EqualTo(Run)); Assert.That(method.Parameters, Is.Empty); - Assert.That(method.ReturnType, Is.EqualTo(type.GetType(Base.None))); + Assert.That(method.ReturnType, Is.EqualTo(type.GetType(Type.None))); Assert.That(method.ToString(), Is.EqualTo(Run)); } @@ -58,7 +58,7 @@ public void ParseFrom() { var method = new Method(type, 0, null!, ["from(number)"]); Assert.That(method.Name, Is.EqualTo("from")); - Assert.That(method.Parameters, Has.Count.EqualTo(1), method.Parameters.ToWordList()); + Assert.That(method.Parameters, Has.Count.EqualTo(1), string.Join(", ", method.Parameters)); Assert.That(method.Parameters[0].Type, Is.EqualTo(type.GetType("Number"))); Assert.That(method.ReturnType, Is.EqualTo(type)); } @@ -71,7 +71,7 @@ public void ParseWithReturnType() var method = new Method(type, 0, null!, NestedMethodLines); Assert.That(method.Name, Is.EqualTo("IsFiveFive")); Assert.That(method.Parameters, Is.Empty); - Assert.That(method.ReturnType, Is.EqualTo(type.GetType(Base.Boolean))); + Assert.That(method.ReturnType, Is.EqualTo(type.GetType(Type.Boolean))); Assert.That(method.ToString(), Is.EqualTo(NestedMethodLines[0])); } @@ -127,7 +127,7 @@ public void NonGenericMethods(string methodHeader) => public void CloningWithSameParameterType() { var method = new Method(type, 0, parser, ["Run(variable Text)", " \"5\""]); - Assert.That(method.Parameters[0].CloneWithImplementationType(type.GetType(Base.Text)), + Assert.That(method.Parameters[0].CloneWithImplementationType(type.GetType(Type.Text)), Is.EqualTo(method.Parameters[0])); } @@ -158,7 +158,7 @@ public void ParseTestsOnlyForGenericShouldReparseFullBody() var expression = customType.Methods[0].GetBodyAndParseIfNeeded(); var body = expression as Body; Assert.That(body != null && body.Expressions.Any(e => - e.GetType().Name == "PlaceholderExpression"), Is.False); + e.GetType().Name == nameof(Method.PlaceholderExpression)), Is.False); } [Test] @@ -181,7 +181,7 @@ public void MethodParameterWithGenericTypeImplementations() Assert.That(method.Parameters[0].Name, Is.EqualTo("iterator")); Assert.That(method.Parameters[0].Type, Is.EqualTo(type.GetType("Iterator(Text)"))); Assert.That(method.Parameters[1].Name, Is.EqualTo("index")); - Assert.That(method.Parameters[1].Type, Is.EqualTo(type.GetType(Base.Number))); + Assert.That(method.Parameters[1].Type, Is.EqualTo(type.GetType(Type.Number))); } [Test] @@ -234,23 +234,10 @@ public void ParameterWithTypeNameAndInitializerIsForbidden() => () => new Method(type, 0, parser, ["Run(input Number = 5)", " 5"]), Throws.InstanceOf()); - [Test] - public void MethodMustHaveAtLeastOneTest() => - Assert.That( - () => - { - using var package = new Package(nameof(MethodMustHaveAtLeastOneTest)); - using var mockType = new Type(package, new MockRunTypeLines()); - return new Method(mockType, 0, parser, ["NoTestMethod Number", " 5"]). - GetBodyAndParseIfNeeded(); - }, //ncrunch: no coverage - Throws.InstanceOf()); - [Test] public void MethodWithTestsAreAllowed() { - using var package = new Package(TestPackage.Instance, nameof(MethodWithTestsAreAllowed)); - using var methodWithTestsType = new Type(package, + using var methodWithTestsType = new Type(TestPackage.Instance, new TypeLines(nameof(MethodWithTestsAreAllowed), "has logger", "MethodWithTestsAreAllowed Number", "\tMethodWithTestsAreAllowed is 5", "\t5")); methodWithTestsType.ParseMembersAndMethods(parser); @@ -260,9 +247,7 @@ public void MethodWithTestsAreAllowed() [Test] public void ParseMethodWithMultipleReturnType() { - using var package = - new Package(TestPackage.Instance, nameof(ParseMethodWithMultipleReturnType)); - using var multipleReturnTypeMethod = new Type(package, new TypeLines("Processor", + using var multipleReturnTypeMethod = new Type(TestPackage.Instance, new TypeLines("Processor", // @formatter:off "has progress Number", "IsJobDone Boolean or Text", @@ -284,9 +269,7 @@ public void ParseMethodWithMultipleReturnType() [Test] public void ParseMethodWithParametersAndMultipleReturnType() { - using var package = new Package(TestPackage.Instance, - nameof(ParseMethodWithParametersAndMultipleReturnType)); - using var multipleReturnTypeMethod = new Type(package, new TypeLines("Processor", + using var multipleReturnTypeMethod = new Type(TestPackage.Instance, new TypeLines("Processor", // @formatter:off "has progress Number", "IsJobDone(number, text) Boolean or Text", @@ -306,9 +289,7 @@ public void ParseMethodWithParametersAndMultipleReturnType() [Test] public void MethodCallWithMultipleReturnTypes() { - using var package = - new Package(TestPackage.Instance, nameof(MethodCallWithMultipleReturnTypes)); - using var multipleReturnTypeMethod = new Type(package, new TypeLines("Processor", + using var multipleReturnTypeMethod = new Type(TestPackage.Instance, new TypeLines("Processor", // @formatter:off "has progress Number", "IsJobDone Boolean or Text", @@ -361,7 +342,7 @@ public void MutableUsesConstantValue() [Test] public void GetVariableUsageCount() => Assert.That( - TestPackage.Instance.GetType(Base.Character).AvailableMethods["to"][0]. + TestPackage.Instance.GetType(Type.Character).AvailableMethods["to"][0]. GetVariableUsageCount("notANumber"), Is.EqualTo(3)); [Test] diff --git a/Strict.Language.Tests/MockRunFileData.cs b/Strict.Language.Tests/MockRunFileData.cs index 37b1b7c8..a16062c6 100644 --- a/Strict.Language.Tests/MockRunFileData.cs +++ b/Strict.Language.Tests/MockRunFileData.cs @@ -2,6 +2,6 @@ namespace Strict.Language.Tests; public sealed class MockRunTypeLines : TypeLines { - public MockRunTypeLines(string name = nameof(MockRunTypeLines)) : base(name, "has logger", "Run", - "\tlog.WriteLine") { } + public MockRunTypeLines(string name = nameof(MockRunTypeLines)) : base(name, "has logger", + "Run", "\tlog.WriteLine") { } } \ No newline at end of file diff --git a/Strict.Language.Tests/PackageTests.cs b/Strict.Language.Tests/PackageTests.cs index 7b01c36a..33f7a10f 100644 --- a/Strict.Language.Tests/PackageTests.cs +++ b/Strict.Language.Tests/PackageTests.cs @@ -19,13 +19,13 @@ public void CreateContexts() private Type publicSubType = null!; [TearDown] - public void TearDown() => ((Package)mainPackage.Parent).Remove(mainPackage); + public void TearDown() => mainPackage.Dispose(); [Test] public void NoneIsAlwaysKnown() { var emptyPackage = new Package(nameof(NoneIsAlwaysKnown)); - Assert.That(emptyPackage.FindType(Base.None, emptyPackage), Is.Not.Null); + Assert.That(emptyPackage.FindType(Type.None, emptyPackage), Is.Not.Null); Assert.That(emptyPackage.FindType(nameof(NoneIsAlwaysKnown), emptyPackage), Is.Null); } @@ -37,22 +37,25 @@ public void IsPrivateNameCheckShouldReturnNull() => [Test] public void RootPackageToStringShouldNotCrash() { - Assert.That(mainType.Package.Parent.ToString(), Is.Empty); - Assert.That(mainType.Package.Parent.FindType(Base.None)?.Name, Is.EqualTo(Base.None)); + Assert.That(mainType.Package.Parent.FullName, Is.Empty); + Assert.That(mainType.Package.Parent.FindType(Type.None)?.Name, Is.EqualTo(Type.None)); Assert.That(mainPackage.Parent.GetPackage(), Is.Null); } [Test] public void GetFullNames() { - Assert.That(mainPackage.ToString(), Is.EqualTo(nameof(PackageTests))); - Assert.That(mainType.ToString(), Is.EqualTo(nameof(PackageTests) + "." + mainType.Name)); - Assert.That(subPackage.ToString(), - Is.EqualTo(nameof(PackageTests) + "." + nameof(subPackage))); - Assert.That(privateSubType.ToString(), - Is.EqualTo(nameof(PackageTests) + "." + nameof(subPackage) + "." + privateSubType.Name)); - Assert.That(publicSubType.ToString(), - Is.EqualTo(nameof(PackageTests) + "." + nameof(subPackage) + "." + publicSubType.Name)); + Assert.That(mainPackage.FullName, Is.EqualTo(nameof(PackageTests))); + Assert.That(mainType.FullName, + Is.EqualTo(nameof(PackageTests) + Context.ParentSeparator + mainType.Name)); + Assert.That(subPackage.FullName, + Is.EqualTo(nameof(PackageTests) + Context.ParentSeparator + nameof(subPackage))); + Assert.That(privateSubType.FullName, + Is.EqualTo(nameof(PackageTests) + Context.ParentSeparator + nameof(subPackage) + + Context.ParentSeparator + privateSubType.Name)); + Assert.That(publicSubType.FullName, + Is.EqualTo(nameof(PackageTests) + Context.ParentSeparator + nameof(subPackage) + + Context.ParentSeparator + publicSubType.Name)); } [Test] @@ -60,22 +63,22 @@ public void PrivateTypesCanOnlyBeFoundInPackageTheyAreIn() { Assert.That(mainType.GetType(publicSubType.Name), Is.EqualTo(publicSubType)); Assert.Throws(() => - mainPackage.GetType(privateSubType.ToString())); + mainPackage.GetType(privateSubType.FullName)); Assert.Throws(() => - mainPackage.GetType(nameof(TestPackage) + "." + nameof(PackageTests) + "." + - privateSubType.Name)); + mainPackage.GetType(nameof(TestPackage) + Context.ParentSeparator + nameof(PackageTests) + + Context.ParentSeparator + privateSubType.Name)); } [Test] public void FindSubTypeBothWays() { - Assert.That(mainType.GetType(publicSubType.ToString()), Is.EqualTo(publicSubType)); - Assert.That(publicSubType.GetType(mainType.ToString()), Is.EqualTo(mainType)); + Assert.That(mainType.GetType(publicSubType.FullName), Is.EqualTo(publicSubType)); + Assert.That(publicSubType.GetType(mainType.FullName), Is.EqualTo(mainType)); } [Test] public void FindPackage() => - Assert.That(mainPackage.Find(subPackage.ToString()), Is.EqualTo(subPackage)); + Assert.That(mainPackage.Find(subPackage.FullName), Is.EqualTo(subPackage)); [Test] public void FindUnknownPackage() => @@ -126,11 +129,11 @@ public async Task LoadTypesFromOtherPackage() { expressionParser.CreateType(); using var strictPackage = await new Repositories(expressionParser).LoadStrictPackage(); - Assert.That(mainPackage.GetType(Base.Number), - Is.EqualTo(strictPackage.GetType(Base.Number)).Or. - EqualTo(subPackage.GetType(Base.Number))); - Assert.That(mainPackage.GetType(Base.Character), - Is.Not.EqualTo(mainPackage.FindType(Base.App))); + Assert.That(mainPackage.GetType(Type.Number), + Is.EqualTo(strictPackage.GetType(Type.Number)).Or. + EqualTo(subPackage.GetType(Type.Number))); + Assert.That(mainPackage.GetType(Type.Character), + Is.Not.EqualTo(mainPackage.FindType(Type.App))); } finally { diff --git a/Strict.Language.Tests/Program.cs b/Strict.Language.Tests/Program.cs index 7a22ae1a..af9f7bb2 100644 --- a/Strict.Language.Tests/Program.cs +++ b/Strict.Language.Tests/Program.cs @@ -6,7 +6,6 @@ public static class Program public static async Task Main() { var tests = new RepositoriesTests(); - await tests.LoadingZippedStrictBaseHundredTimes(); tests.LoadingAllStrictFilesWithoutAsyncHundredTimes(); tests.SortImplementsOneThousandTimesInParallel(); await tests.LoadStrictBaseTypesHundredTimes(); diff --git a/Strict.Language.Tests/RepositoriesTests.cs b/Strict.Language.Tests/RepositoriesTests.cs index bbcbe2b9..1fa62bf8 100644 --- a/Strict.Language.Tests/RepositoriesTests.cs +++ b/Strict.Language.Tests/RepositoriesTests.cs @@ -1,4 +1,3 @@ -using System.IO.Compression; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Running; @@ -26,7 +25,7 @@ public void CreateRepositories() [Test] public void InvalidPathWontWork() => Assert.ThrowsAsync(() => - repos.LoadFromPath(nameof(InvalidPathWontWork))); + repos.LoadFromPath(nameof(InvalidPathWontWork), nameof(InvalidPathWontWork))); [Test] public void LoadingNonGithubPackageWontWork() => @@ -36,10 +35,12 @@ public void LoadingNonGithubPackageWontWork() => [Test] public async Task LoadStrictBaseTypes() { + Assert.That(repos.ContainsPackageNameInCache(nameof(Strict)), Is.False, + await repos.ToDebugString()); using var basePackage = await repos.LoadStrictPackage(); - Assert.That(basePackage.FindDirectType(Base.Any), Is.Not.Null); - Assert.That(basePackage.FindDirectType(Base.Number), Is.Not.Null); - Assert.That(basePackage.FindDirectType(Base.App), Is.Not.Null); + Assert.That(basePackage.FindDirectType(Type.Any), Is.Not.Null); + Assert.That(basePackage.FindDirectType(Type.Number), Is.Not.Null); + Assert.That(basePackage.FindDirectType(Type.App), Is.Not.Null); } [Test] @@ -63,7 +64,8 @@ public async Task MakeSureParsingFailedErrorMessagesAreClickable() using var _ = new Type(strictPackage, new TypeLines("Invalid", "has 1")). ParseMembersAndMethods(null!); }, //ncrunch: no coverage - Throws.InstanceOf().With.Message.Contains(@"Base\Invalid.strict:line 1")); + Throws.InstanceOf().With.Message. + Contains(Path.Combine("Strict", "Invalid.strict") + ":line 1")); } //ncrunch: no coverage start @@ -78,8 +80,8 @@ public async Task LoadStrictExamplesPackageAndUseBasePackageTypes() new Type(examplesPackage, new TypeLines("ValidProgram", "has number", "Run Number", "\tnumber")). ParseMembersAndMethods(parser); - Assert.That(program.Methods[0].ReturnType.ToString(), Contains.Substring(Base.Number)); - Assert.That(program.Members[0].Type.ToString(), Contains.Substring(Base.Number)); + Assert.That(program.Methods[0].ReturnType.ToString(), Contains.Substring(Type.Number)); + Assert.That(program.Members[0].Type.ToString(), Contains.Substring(Type.Number)); } [Test] @@ -87,9 +89,11 @@ public async Task LoadStrictExamplesPackageAndUseBasePackageTypes() public async Task LoadStrictImageProcessingTypes() { using var basePackage = await repos.LoadStrictPackage(); - using var mathPackage = await repos.LoadFromPath(StrictDevelopmentFolder + ".Math"); - using var imageProcessingPackage = - await repos.LoadFromPath(StrictDevelopmentFolder + ".ImageProcessing"); + using var mathPackage = await repos.LoadFromPath(nameof(Strict) + ".Math", + Repositories.GetLocalDevelopmentPath(Repositories.StrictOrg, nameof(Strict) + ".Math")); + using var imageProcessingPackage = await repos.LoadFromPath(nameof(Strict) + ".ImageProcessing", + Repositories.GetLocalDevelopmentPath(Repositories.StrictOrg, + nameof(Strict) + ".ImageProcessing")); var adjustBrightness = imageProcessingPackage.GetType("AdjustBrightness"); Assert.That(adjustBrightness, Is.Not.Null); Assert.That(adjustBrightness.Methods[0].GetBodyAndParseIfNeeded(), Is.Not.Null); @@ -98,10 +102,10 @@ public async Task LoadStrictImageProcessingTypes() [Test] public async Task CheckGenericTypesAreLoadedCorrectlyAfterSorting() { - using var program = - new Type(await repos.LoadStrictPackage(), - new TypeLines("ValidProgram", "has texts", "Run Texts", "\t\"Result \" + 5")). - ParseMembersAndMethods(parser); + using var package = await repos.LoadStrictPackage(); + using var program = new Type(package, + new TypeLines("ValidProgram", "has texts", "Run Texts", "\t\"Result \" + 5")). + ParseMembersAndMethods(parser); program.Methods[0].GetBodyAndParseIfNeeded(); Assert.That(program.Members[0].Type.IsIterator, Is.True); Assert.That(program.Members[0].Type.Members.Count, Is.GreaterThan(1)); @@ -145,19 +149,6 @@ private static Dictionary CreateComplexImplementsDependencies } //ncrunch: no coverage start - [Category("Manual")] - [Test] - public void NoFilesAllowedInStrictFolderNeedsToBeInASubFolder() - { - var strictFilePath = Path.Combine(StrictDevelopmentFolder, "UnitTestForCoverage.strict"); - File.Create(strictFilePath).Close(); - Assert.That(() => repos.LoadFromPath(StrictDevelopmentFolder), - Throws.InstanceOf()); - File.Delete(strictFilePath); - } - - public static readonly string StrictDevelopmentFolder = Repositories.StrictDevelopmentFolderPrefix[..^1]; - [Test] [Category("Slow")] [Benchmark] @@ -194,29 +185,12 @@ public void SortImplementsOneThousandTimesInParallel() public void LoadingAllStrictFilesWithoutAsyncHundredTimes() { for (var iteration = 0; iteration < 100; iteration++) - foreach (var file in Directory.GetFiles(BaseFolder, "*.strict")) + foreach (var file in Directory.GetFiles( + Repositories.GetLocalDevelopmentPath(Repositories.StrictOrg, nameof(Strict)), + "*" + Type.Extension)) File.ReadAllLines(file); } - private static string BaseFolder => Repositories.StrictDevelopmentFolderPrefix + nameof(Base); - - [Test] - [Category("Slow")] - [Benchmark] - public async Task LoadingZippedStrictBaseHundredTimes() - { - var zipFilePath = Path.Combine(StrictDevelopmentFolder, "Base.zip"); - if (!File.Exists(zipFilePath)) - ZipFile.CreateFromDirectory(BaseFolder, zipFilePath); - for (var iteration = 0; iteration < 100; iteration++) - { - var tasks = new List(); - foreach (var entry in ZipFile.OpenRead(zipFilePath).Entries) - tasks.Add(new StreamReader(entry.Open()).ReadToEndAsync()); - await Task.WhenAll(tasks); - } - } - [Test] [Category("Slow")] [Benchmark] diff --git a/Strict.Language.Tests/Strict.Language.Tests.csproj b/Strict.Language.Tests/Strict.Language.Tests.csproj index c18bbab1..63b2da72 100644 --- a/Strict.Language.Tests/Strict.Language.Tests.csproj +++ b/Strict.Language.Tests/Strict.Language.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/Strict.Language.Tests/Strict.Language.Tests.v3.ncrunchproject b/Strict.Language.Tests/Strict.Language.Tests.v3.ncrunchproject index a1a4bdb3..027c9fb1 100644 --- a/Strict.Language.Tests/Strict.Language.Tests.v3.ncrunchproject +++ b/Strict.Language.Tests/Strict.Language.Tests.v3.ncrunchproject @@ -10,6 +10,9 @@ Strict.Language.Tests.RepositoriesTests.LoadingZippedStrictBaseHundredTimes + + Strict.Language.Tests.RepositoriesTests.LoadingZippedStrictHundredTimes + \ No newline at end of file diff --git a/Strict.Language.Tests/StringExtensionsTests.cs b/Strict.Language.Tests/StringExtensionsTests.cs index 65ea5820..a0b30ffa 100644 --- a/Strict.Language.Tests/StringExtensionsTests.cs +++ b/Strict.Language.Tests/StringExtensionsTests.cs @@ -1,5 +1,3 @@ -using System.Collections; - namespace Strict.Language.Tests; public sealed class StringExtensionsTests @@ -112,20 +110,7 @@ public void StartsWith() } [Test] - public void ToWordList() - { - Assert.That(new List { "hi", "there" }.ToWordList(), Is.EqualTo("hi, there")); - Assert.That(new[] { 1, 2, 3 }.ToWordList(), Is.EqualTo("1, 2, 3")); + public void DictionaryToWordList() => Assert.That(new Dictionary { { "number", 5 }, { "values", new[] { 0, 1, 2 } } }. DictionaryToWordList(), Is.EqualTo("number=5; values=0, 1, 2")); - IDictionary dict = new Hashtable - { - { "name", "Kata" }, - { "ids", new List { 1, 2, 3 } } - }; - Assert.That(dict.EnumerableToWordList(), - Does.Contain("name=Kata").And.Contains("ids=1, 2, 3")); - IEnumerable values = new ArrayList { "apple", "banana", "cherry" }; - Assert.That(values.EnumerableToWordList(), Is.EqualTo("apple, banana, cherry")); - } } \ No newline at end of file diff --git a/Strict.Language.Tests/TestPackage.cs b/Strict.Language.Tests/TestPackage.cs index 4b3be675..031c01bf 100644 --- a/Strict.Language.Tests/TestPackage.cs +++ b/Strict.Language.Tests/TestPackage.cs @@ -3,7 +3,7 @@ namespace Strict.Language.Tests; /// /// Helper context to provide a bunch of helper types to make tests work. /// -public class TestPackage : Package +public sealed class TestPackage : Package { public static readonly Package Instance = new TestPackage(); @@ -11,9 +11,9 @@ private TestPackage() : base(nameof(TestPackage)) { var parser = new MethodExpressionParser(); // @formatter:off - var any = new Type(this, new TypeLines(Base.Any, + var any = new Type(this, new TypeLines(Type.Any, "from", "to Type", "to Text", "is(other) Boolean")); - var boolean = new Type(this, new TypeLines(Base.Boolean, + var boolean = new Type(this, new TypeLines(Type.Boolean, "not Boolean", "\tnot true is false", "\tvalue then false else true", @@ -28,7 +28,7 @@ private TestPackage() : base(nameof(TestPackage)) "\tfalse xor false is false", "\tvalue and other or (not value) and (not other) then false else true")); var hasLength = new Type(this, new TypeLines("HasLength","Length Number")); - var number = new Type(this, new TypeLines(Base.Number, + var number = new Type(this, new TypeLines(Type.Number, "to Character", "\t5 to Character is \"5\"", "\tconstant canOnlyConvertSingleDigit = Error", @@ -68,7 +68,7 @@ private TestPackage() : base(nameof(TestPackage)) "\tvalue = value + 1", "Decrement Mutable(Number)", "\tvalue = value - 1")); - var range = new Type(this, new TypeLines(Base.Range, + var range = new Type(this, new TypeLines(Type.Range, "has iterator", "has Start Number", "has ExclusiveEnd Number", @@ -94,7 +94,7 @@ private TestPackage() : base(nameof(TestPackage)) "\tRange(10, 5).Reverse is Range(6, 11)", "\tRange(-5, -10).Reverse is Range(-9, -4)", "\tLength > 0 then Range(ExclusiveEnd - 1, Start - 1) else Range(ExclusiveEnd + 1, Start + 1)")); - var character = new Type(this, new TypeLines(Base.Character, + var character = new Type(this, new TypeLines(Type.Character, "has number", "constant zeroCharacter = 48", "constant NewLine = Character(13)", @@ -105,13 +105,13 @@ private TestPackage() : base(nameof(TestPackage)) "\tCharacter(\"A\") to Number is notANumber", "\tlet result = number - zeroCharacter", "\tresult is in Range(0, 10) then result else notANumber(value)")); - var mutable = new Type(this, new TypeLines(Base.Mutable, + var mutable = new Type(this, new TypeLines(Type.Mutable, "has generic")); - var iterator = new Type(this, new TypeLines(Base.Iterator, + var iterator = new Type(this, new TypeLines(Type.Iterator, "for Iterator(Generic)", "in(element Generic) Boolean", "Length Number")); - var list = new Type(this, new TypeLines(Base.List, + var list = new Type(this, new TypeLines(Type.List, "has iterator", "has elements Generics", "Length Number", @@ -189,7 +189,7 @@ private TestPackage() : base(nameof(TestPackage)) "\t(5, 10, 5).Reverse is (5, 10, 5)", "\tfor Range(0, value.Length).Reverse", "\t\touter.value(index)")); - var text = new Type(this, new TypeLines(Base.Text, + var text = new Type(this, new TypeLines(Type.Text, "has characters", "from(number)", "\tvalue", @@ -209,32 +209,32 @@ private TestPackage() : base(nameof(TestPackage)) "\t\"Hey\" is \"Hey\"", "\t\"Hi\" is not \"Hey\"", "\tvalue is other")); - var baseType = new Type(this, new TypeLines(Base.Type, "has Name", + var baseType = new Type(this, new TypeLines(nameof(Type), "has Name", "has Package Text", "to Text", "\tPackage + \".\" + Name")); - var generic = new Type(this, new TypeLines(Base.Generic, "from(type)")); - var logger = new Type(this, new TypeLines(Base.Logger, + var generic = new Type(this, new TypeLines(Type.GenericUppercase, "from(type)")); + var logger = new Type(this, new TypeLines(Type.Logger, "has textWriter", "Log(text)", "\ttextWriter.Write(text)")); - var file = new Type(this, new TypeLines(Base.File, + var file = new Type(this, new TypeLines(Type.File, "from(text)", "Read Text", "Write(text)", "Delete", "Length Number")); - var textWriter = new Type(this, new TypeLines(Base.TextWriter, "Write(text)")); - var textReader = new Type(this, new TypeLines(Base.TextReader, "Read Text")); - var name = new Type(this, new TypeLines(Base.Name, "has text")); - var error = new Type(this, new TypeLines(Base.Error, "has Name", "has Stacktraces", + var textWriter = new Type(this, new TypeLines(Type.TextWriter, "Write(text)")); + var textReader = new Type(this, new TypeLines(Type.TextReader, "Read Text")); + var name = new Type(this, new TypeLines(nameof(Name), "has text")); + var error = new Type(this, new TypeLines(Type.Error, "has Name", "has Stacktraces", "Text Text", "\tName to Text")); - var errorWithValue = new Type(this, new TypeLines(Base.ErrorWithValue, "has Error", + var errorWithValue = new Type(this, new TypeLines(Type.ErrorWithValue, "has Error", "has Value Generic")); - var method = new Type(this, new TypeLines(Base.Method, "has Name", "has Type")); - var stacktrace = new Type(this, new TypeLines(Base.Stacktrace, + var method = new Type(this, new TypeLines(nameof(Method), "has Name", "has Type")); + var stacktrace = new Type(this, new TypeLines(Type.Stacktrace, "has Method", "has FilePath Text", "has Line Number")); - var dictionary = new Type(this, new TypeLines(Base.Dictionary, + var dictionary = new Type(this, new TypeLines(Type.Dictionary, "has keysAndValues List(key Generic, mappedValue Generic)", "from", "\tDictionary(Number, Number).Length is 0", diff --git a/Strict.Language.Tests/TypeLinesTests.cs b/Strict.Language.Tests/TypeLinesTests.cs index 40dda5ce..3b178887 100644 --- a/Strict.Language.Tests/TypeLinesTests.cs +++ b/Strict.Language.Tests/TypeLinesTests.cs @@ -5,39 +5,39 @@ public class TypeLinesTests [Test] public void ListMembersShouldBeExtractedCorrectly() { - var type = new TypeLines(Base.Text, "has characters"); + var type = new TypeLines(Type.Text, "has characters"); Assert.That(type.DependentTypes.Count, Is.EqualTo(2)); - Assert.That(type.DependentTypes[0], Is.EqualTo(Base.List)); - Assert.That(type.DependentTypes[1], Is.EqualTo(Base.Character)); + Assert.That(type.DependentTypes[0], Is.EqualTo(Type.List)); + Assert.That(type.DependentTypes[1], Is.EqualTo(Type.Character)); } [Test] public void TextWriterShouldBeUppercase() { - var type = new TypeLines(Base.Logger, "has textWriter"); + var type = new TypeLines(Type.Logger, "has textWriter"); Assert.That(type.DependentTypes.Count, Is.EqualTo(1)); - Assert.That(type.DependentTypes[0], Is.EqualTo(Base.TextWriter)); + Assert.That(type.DependentTypes[0], Is.EqualTo(Type.TextWriter)); } [Test] public void RangeHasIteratorAndNumber() { - var type = new TypeLines(Base.Range, "has iterator", "has Start Number", "has End Number"); + var type = new TypeLines(Type.Range, "has iterator", "has Start Number", "has End Number"); Assert.That(type.DependentTypes.Count, Is.EqualTo(2)); - Assert.That(type.DependentTypes[0], Is.EqualTo(Base.Iterator)); - Assert.That(type.DependentTypes[1], Is.EqualTo(Base.Number)); + Assert.That(type.DependentTypes[0], Is.EqualTo(Type.Iterator)); + Assert.That(type.DependentTypes[1], Is.EqualTo(Type.Number)); } [Test] public void MethodReturnTypeShouldBeExtractedIntoDependentTypes() { - var typeLines = new TypeLines(Base.Directory, + var typeLines = new TypeLines(Type.Directory, "GetFile Text", "GetFiles Texts", "GetDirectories Texts"); Assert.That(typeLines.DependentTypes.Count, Is.EqualTo(2), string.Join(",", typeLines.DependentTypes)); - Assert.That(typeLines.DependentTypes[0], Is.EqualTo(Base.Text)); - Assert.That(typeLines.DependentTypes[1], Is.EqualTo(Base.List)); + Assert.That(typeLines.DependentTypes[0], Is.EqualTo(Type.Text)); + Assert.That(typeLines.DependentTypes[1], Is.EqualTo(Type.List)); } } \ No newline at end of file diff --git a/Strict.Language.Tests/TypeMethodFinderTests.cs b/Strict.Language.Tests/TypeMethodFinderTests.cs index 606660e3..4f8d1e1e 100644 --- a/Strict.Language.Tests/TypeMethodFinderTests.cs +++ b/Strict.Language.Tests/TypeMethodFinderTests.cs @@ -8,7 +8,7 @@ public sealed class TypeMethodFinderTests public void CreatePackage() { parser = new MethodExpressionParser(); - appType = CreateType(Base.App, "Run"); + appType = CreateType(Type.App, "Run"); } private Type CreateType(string name, params string[] lines) => @@ -18,7 +18,7 @@ private Type CreateType(string name, params string[] lines) => private Type appType = null!; [TearDown] - public void TearDown() => TestPackage.Instance.Remove(appType); + public void TearDown() => appType.Dispose(); [Test] public void CanUpCastNumberWithList() @@ -32,7 +32,7 @@ public void CanUpCastNumberWithList() ]); Assert.That(result, Is.InstanceOf()); Assert.That(result?.ToString(), - Is.EqualTo("Add(first TestPackage.Number, other TestPackage.List(Number)) List")); + Is.EqualTo("Add(first TestPackage/Number, other TestPackage/List(Number)) List")); } [Test] @@ -58,7 +58,7 @@ public void UsingGenericMethodIsAllowed() ])?. ToString(), Is.EqualTo( - "Add(other TestPackage.List(Text), first TestPackage.Generic) List")); + "Add(other TestPackage/List(Text), first TestPackage/Generic) List")); } [Test] @@ -68,9 +68,9 @@ public void GenericMethodShouldAcceptAllInputTypes() "has logger", "Write(generic)", "\tlogger.Log(generic)"); Assert.That(type.FindMethod("Write", [new Text(type, "hello")])?.ToString(), - Is.EqualTo("Write(generic TestPackage.Generic)")); + Is.EqualTo("Write(generic TestPackage/Generic)")); Assert.That(type.FindMethod("Write", [new Number(type, 5)])?.ToString(), - Is.EqualTo("Write(generic TestPackage.Generic)")); + Is.EqualTo("Write(generic TestPackage/Generic)")); } [Test] @@ -142,24 +142,26 @@ public void MutableTypesOrImplementsShouldNotBeUsedDirectly() [Test] public void RangeTypeShouldHaveCorrectAvailableMethods() { - var range = TestPackage.Instance.GetType(Base.Range); + var range = TestPackage.Instance.GetType(Type.Range); Assert.That(range.AvailableMethods.Values.Select(methods => methods.Count).Sum(), - Is.EqualTo(8), "AvailableMethods: " + range.AvailableMethods.ToWordList()); + Is.EqualTo(8), + "AvailableMethods: " + range.AvailableMethods.DictionaryToWordList("\n")); } [Test] public void TextTypeShouldHaveCorrectAvailableMethods() { - var text = TestPackage.Instance.GetType(Base.Text + "s"); + var text = TestPackage.Instance.GetType(Type.Text + "s"); Assert.That(text.AvailableMethods.Values.Select(methods => methods.Count).Sum(), - Is.GreaterThanOrEqualTo(18), "AvailableMethods: " + text.AvailableMethods.ToWordList("\n")); + Is.GreaterThanOrEqualTo(18), + "AvailableMethods: " + text.AvailableMethods.DictionaryToWordList("\n")); } [Test] public void DictionaryIsComparisonShouldNotThrow() { - var number = TestPackage.Instance.GetType(Base.Number); - var dictionary = TestPackage.Instance.GetType(Base.Dictionary). + var number = TestPackage.Instance.GetType(Type.Number); + var dictionary = TestPackage.Instance.GetType(Type.Dictionary). GetGenericImplementation(number, number); Assert.That(dictionary.FindMethod(BinaryOperator.Is, [new Instance(dictionary)]), Is.Not.Null); @@ -190,19 +192,19 @@ public void AvailableMethodsShouldNotHaveMembersPrivateMethods() type.AvailableMethods.Values.Any(methods => methods.Any(method => !method.IsPublic && !method.Name.AsSpan().IsOperator())), Is.False, // If this fails, check by debugging each private method and see if IsOperator returns true - type.AvailableMethods.ToWordList()); + type.AvailableMethods.DictionaryToWordList("\n")); } [Test] public void IsMutableAndHasMatchingInnerType() { - var number = TestPackage.Instance.GetType(Base.Number); - Assert.That(CreateMutableType(Base.Number).IsSameOrCanBeUsedAs(number), Is.True); - Assert.That(CreateMutableType(Base.Text).IsSameOrCanBeUsedAs(number), Is.False); + var number = TestPackage.Instance.GetType(Type.Number); + Assert.That(CreateMutableType(Type.Number).IsSameOrCanBeUsedAs(number), Is.True); + Assert.That(CreateMutableType(Type.Text).IsSameOrCanBeUsedAs(number), Is.False); } private static Type CreateMutableType(string typeName) => - TestPackage.Instance.GetType(Base.Mutable). + TestPackage.Instance.GetType(Type.Mutable). GetGenericImplementation(TestPackage.Instance.GetType(typeName)); [Test] @@ -248,4 +250,26 @@ public void SingleCharacterTextIsAlwaysValidAsCharacter() "Run", "\t5 to Character is \"5\""); type.GetMethod("Run", []).GetBodyAndParseIfNeeded(); } + + [Test] + public void ConstraintWithLengthGreaterThanZero() + { + using var type = CreateType(nameof(ConstraintWithLengthGreaterThanZero), + "has logger", + "Result(items Generics) Text", + "\titems.Length > 0 then \"Has items\" else \"No items\""); + var method = type.FindMethod("Result", [new List(null!, [new Number(type, 1)])]); + Assert.That(method, Is.Not.Null); + } + + [Test] + public void ConstraintWithExactLength() + { + using var type = CreateType(nameof(ConstraintWithExactLength), + "has logger", + "Pair(items Generics) Text", + "\tvalue.Length is 2 then \"Pair\" else \"Not pair\""); + var method = type.FindMethod("Pair", [new List(null!, [new Number(type, 1), new Number(type, 2)])]); + Assert.That(method, Is.Not.Null); + } } \ No newline at end of file diff --git a/Strict.Language.Tests/TypeTests.cs b/Strict.Language.Tests/TypeTests.cs index f854cf94..53d5e442 100644 --- a/Strict.Language.Tests/TypeTests.cs +++ b/Strict.Language.Tests/TypeTests.cs @@ -9,7 +9,7 @@ public sealed class TypeTests public void CreateParser() { parser = new MethodExpressionParser(); - appType = CreateType(Base.App, "Run"); + appType = CreateType(Type.App, "Run"); } private Type CreateType(string name, params string[] lines) => @@ -24,7 +24,7 @@ private Type CreateType(string name, params string[] lines) => [Test] public void AddingTheSameNameIsNotAllowed() => - Assert.That(() => CreateType(Base.App, "Run"), + Assert.That(() => CreateType(Type.App, "Run"), Throws.InstanceOf()); [Test] @@ -59,7 +59,7 @@ public void TypeNotFound(params string[] lines) => [Test] public void NoMethodsFound() => Assert.That( - () => new Type(new Package(nameof(NoMethodsFound)), new TypeLines("dummy", "has Number")). + () => new Type(TestPackage.Instance, new TypeLines("dummy", "has Number")). ParseMembersAndMethods(null!), Throws.InstanceOf()); [Test] @@ -71,7 +71,7 @@ public void NoMatchingMethodFound() => [Test] public void TypeNameMustBeWord() => - Assert.That(() => new Member(package.GetType(Base.App), "blub7", null!), + Assert.That(() => new Member(package.GetType(Type.App), "blub7", null!), Throws.InstanceOf()); [Test] @@ -118,7 +118,7 @@ public void SimpleApp() => private static void CheckApp(Type program) { - Assert.That(program.Members[0].Type.Name, Is.EqualTo(Base.App)); + Assert.That(program.Members[0].Type.Name, Is.EqualTo(Type.App)); Assert.That(program.Members[1].Name, Is.EqualTo("logger")); Assert.That(program.Methods[0].Name, Is.EqualTo("Run")); Assert.That(program.IsTrait, Is.False); @@ -159,8 +159,8 @@ public void Trait() Assert.That(app.Methods[0].Name, Is.EqualTo("Run")); } - [TestCase(Base.Number, "has number", "Run", "\tmutable result = 2", "\tresult = result + 2")] - [TestCase(Base.Text, "has number", "Run", "\tmutable result = \"2\"", "\tresult = result + \"!\"")] + [TestCase(Type.Number, "has number", "Run", "\tmutable result = 2", "\tresult = result + 2")] + [TestCase(Type.Text, "has number", "Run", "\tmutable result = \"2\"", "\tresult = result + \"!\"")] public void MutableTypesHaveProperDataReturnType(string expected, params string[] code) { using var type = new Type(package, new TypeLines(nameof(MutableTypesHaveProperDataReturnType), code)); @@ -214,24 +214,24 @@ public void ValueTypeNotMatchingWithAssignmentType() => [Test] public void MakeSureGenericTypeIsProperlyGenerated() { - var listType = package.GetType(Base.List); + var listType = package.GetType(Type.List); Assert.That(listType.IsGeneric, Is.True); - Assert.That(listType.Members[0].Type, Is.EqualTo(package.GetType(Base.Iterator))); + Assert.That(listType.Members[0].Type, Is.EqualTo(package.GetType(Type.Iterator))); using var type = new Type(package, new TypeLines(nameof(MakeSureGenericTypeIsProperlyGenerated), "has numbers", "GetNumbers Numbers", "\tnumbers")); var getNumbersBody = type.ParseMembersAndMethods(parser).Methods[0]. GetBodyAndParseIfNeeded(); - var numbersType = package.GetListImplementationType(package.GetType(Base.Number)); + var numbersType = package.GetListImplementationType(package.GetType(Type.Number)); Assert.That(getNumbersBody.ReturnType, Is.EqualTo(numbersType)); - Assert.That(numbersType.Generic, Is.EqualTo(package.GetType(Base.List))); - Assert.That(numbersType.ImplementationTypes[0], Is.EqualTo(package.GetType(Base.Number))); + Assert.That(numbersType.Generic, Is.EqualTo(package.GetType(Type.List))); + Assert.That(numbersType.ImplementationTypes[0], Is.EqualTo(package.GetType(Type.Number))); } [Test] public void CannotGetGenericImplementationOnNonGenericType() => Assert.That( - () => package.GetType(Base.Text).GetGenericImplementation(package.GetType(Base.Number)), + () => package.GetType(Type.Text).GetGenericImplementation(package.GetType(Type.Number)), Throws.InstanceOf()); [Test] @@ -252,13 +252,13 @@ public void InvalidProgram() => "\tconstant result = list + 5")).ParseMembersAndMethods(null!), Throws.InstanceOf()); - [TestCase(Base.TextWriter, 0)] - [TestCase(Base.Mutable, 1)] - [TestCase(Base.Logger, 1)] - [TestCase(Base.Number, 0)] - [TestCase(Base.Character, 2)] - [TestCase(Base.Text, 2)] - [TestCase(Base.Error, 5)] + [TestCase(Type.TextWriter, 0)] + [TestCase(Type.Mutable, 1)] + [TestCase(Type.Logger, 1)] + [TestCase(Type.Number, 0)] + [TestCase(Type.Character, 2)] + [TestCase(Type.Text, 2)] + [TestCase(Type.Error, 5)] public void ValidateAvailableMemberTypesCount(string name, int expectedCount) { var type = package.GetType(name); @@ -351,8 +351,8 @@ public void AppleTypeCompatibilityCheck() using var redApple = CreateType("RedApple", "has apple", "Color Text", "\tvalue.Color"); Assert.That(apple.IsSameOrCanBeUsedAs(redApple), Is.False); Assert.That(redApple.IsSameOrCanBeUsedAs(apple), Is.True); - Assert.That(redApple.IsSameOrCanBeUsedAs(package.GetType(Base.Text)), Is.True); - Assert.That(redApple.IsSameOrCanBeUsedAs(package.GetType(Base.Number)), Is.False); + Assert.That(redApple.IsSameOrCanBeUsedAs(package.GetType(Type.Text)), Is.True); + Assert.That(redApple.IsSameOrCanBeUsedAs(package.GetType(Type.Number)), Is.False); } [Test] @@ -360,15 +360,15 @@ public void FileLoggerIsCompatibleWithFileAndLogger() { using var logger = CreateType("FileLogger", "has source File", "has logger", "Log Number", "\tvalue"); - Assert.That(logger.IsSameOrCanBeUsedAs(package.GetType(Base.File)), Is.True); - Assert.That(logger.IsSameOrCanBeUsedAs(package.GetType(Base.Logger)), Is.True); + Assert.That(logger.IsSameOrCanBeUsedAs(package.GetType(Type.File)), Is.True); + Assert.That(logger.IsSameOrCanBeUsedAs(package.GetType(Type.Logger)), Is.True); } [Test] public void AccountantIsNotCompatibleWithFile() { using var accountant = CreateType("Accountant", "has taxFile File", "has assetFile File", "Calculate Number", "\tvalue"); - Assert.That(accountant.IsSameOrCanBeUsedAs(package.GetType(Base.File)), Is.False); + Assert.That(accountant.IsSameOrCanBeUsedAs(package.GetType(Type.File)), Is.False); } [Test] @@ -376,7 +376,7 @@ public void EnumCanBeUsedAsNumber() { using var instructionType = new Type(package, new TypeLines("Instruction", "constant Set", "constant Add")).ParseMembersAndMethods(parser); - Assert.That(instructionType.IsSameOrCanBeUsedAs(package.GetType(Base.Number)), Is.True); + Assert.That(instructionType.IsSameOrCanBeUsedAs(package.GetType(Type.Number)), Is.True); } [Test] @@ -387,40 +387,40 @@ public void MemberNameAsAnotherMemberTypeNameIsForbidden() => Throws.InstanceOf().Or. InstanceOf()); - [TestCase(Base.Number, false)] - [TestCase(Base.Number + "s", true)] - [TestCase(Base.Character, false)] - [TestCase(Base.Character + "s", true)] - [TestCase(Base.Text, true)] - [TestCase(Base.Text + "s", true)] - [TestCase(Base.Boolean, false)] + [TestCase(Type.Number, false)] + [TestCase(Type.Number + "s", true)] + [TestCase(Type.Character, false)] + [TestCase(Type.Character + "s", true)] + [TestCase(Type.Text, true)] + [TestCase(Type.Text + "s", true)] + [TestCase(Type.Boolean, false)] public void ValidateIsIterator(string name, bool expected) => Assert.That(package.GetType(name).IsIterator, Is.EqualTo(expected)); [Test] public void FindLineNumber() => - Assert.That(package.GetType(Base.Number).FindLineNumber("to Text"), Is.EqualTo(28)); + Assert.That(package.GetType(Type.Number).FindLineNumber("to Text"), Is.EqualTo(28)); [Test] public void FindFirstUnionTypeReturnsThisWhenElseTypeIsError() { - var errorType = package.GetType(Base.Error); + var errorType = package.GetType(Type.Error); Assert.That(appType.FindFirstUnionType(errorType), Is.EqualTo(appType)); } [Test] public void FindFirstUnionTypeReturnsElseTypeWhenCurrentTypeIsError() { - var errorType = package.GetType(Base.Error); - var textType = package.GetType(Base.Text); + var errorType = package.GetType(Type.Error); + var textType = package.GetType(Type.Text); Assert.That(errorType.FindFirstUnionType(textType), Is.EqualTo(textType)); } [Test] public void FindFirstUnionTypeReturnsIteratorWhenElseTypeIsNumber() { - var iteratorType = package.GetType(Base.List); - var numberType = package.GetType(Base.Number); + var iteratorType = package.GetType(Type.List); + var numberType = package.GetType(Type.Number); Assert.That(iteratorType.FindFirstUnionType(numberType), Is.EqualTo(iteratorType)); } @@ -432,7 +432,7 @@ public void FindFirstUnionTypeReturnsSharedMemberType() using var secondType = CreateType(nameof(FindFirstUnionTypeReturnsSharedMemberType) + "Second", "has number", "Run", "\t1"); Assert.That(firstType.FindFirstUnionType(secondType), - Is.EqualTo(package.GetType(Base.Number))); + Is.EqualTo(package.GetType(Type.Number))); } [Test] diff --git a/Strict.Language/Base.cs b/Strict.Language/Base.cs deleted file mode 100644 index 8abb2661..00000000 --- a/Strict.Language/Base.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace Strict.Language; - -/// -/// Simple helper to give us all the names of common base types often used like Number and Boolean -/// BinaryOperators has all the operators defined and Type has: generic, value, index, other, outer -/// -public static class Base -{ - /// - /// Has no implementation and is used for void, empty or none, which is not valid to assign. - /// - public const string None = nameof(None); - /// - /// Defines all the methods available in any type (everything automatically implements **Any**). - /// These methods don't have to be implemented by any class, they are automatically implemented. - /// - public const string Any = nameof(Any); - /// - /// Most basic type: can only be true or false, any statement must either be None or return a - /// Boolean (anything else is a compiler error). Any statement returning false (like a failing - /// test) will also immediately cause an error at runtime or in the Editor via SCrunch. - /// - public const string Boolean = nameof(Boolean); - /// - /// Can be any floating point or integer number (think byte, short, int, long, float or double - /// in other languages). Also, it can be a decimal or BigInteger, the compiler can decide and - /// optimize this away into anything that makes sense in the current context. - /// - public const string Number = nameof(Number); - public const string Character = nameof(Character); - public const string HashCode = nameof(HashCode); - public const string Range = nameof(Range); - public const string Text = nameof(Text); - public const string Error = nameof(Error); - public const string ErrorWithValue = nameof(ErrorWithValue); - public const string Generic = nameof(Generic); - public const string Iterator = nameof(Iterator); - public const string List = nameof(List); - public const string Type = nameof(Type); - public const string Method = nameof(Method); - public const string Logger = nameof(Logger); - public const string App = nameof(App); - public const string System = nameof(System); - public const string File = nameof(File); - public const string Return = nameof(Return); - public const string Directory = nameof(Directory); - public const string Run = nameof(Run); - public const string Declaration = nameof(Declaration); - public const string MutableReassignment = nameof(MutableReassignment); - public const string TextWriter = nameof(TextWriter); - public const string TextReader = nameof(TextReader); - public const string Stacktrace = nameof(Stacktrace); - public const string Name = nameof(Name); - public const string Mutable = nameof(Mutable); - public const string Dictionary = nameof(Dictionary); -} \ No newline at end of file diff --git a/Strict.Language/BinaryOperator.cs b/Strict.Language/BinaryOperator.cs index b0d00beb..478a0ffc 100644 --- a/Strict.Language/BinaryOperator.cs +++ b/Strict.Language/BinaryOperator.cs @@ -3,7 +3,7 @@ namespace Strict.Language; /// -/// https://strict.dev/docs/Keywords +/// https://strict-lang.org/docs/Keywords /// public static class BinaryOperator { diff --git a/Strict.Language/Body.cs b/Strict.Language/Body.cs index cd1dee02..2a468883 100644 --- a/Strict.Language/Body.cs +++ b/Strict.Language/Body.cs @@ -1,4 +1,6 @@ +#if DEBUG using System.Diagnostics; +#endif using System.Runtime.CompilerServices; using static Strict.Language.Method; @@ -74,7 +76,7 @@ public Expression Parse() private void UpdateValueTypeForPiping(Expression lastExpression) { - if (lastExpression.ReturnType.Name == Base.None) + if (lastExpression.ReturnType.IsNone) return; var valueVar = FindVariable(Type.ValueLowercase.AsSpan(), false); if (valueVar == null || valueVar.Type == lastExpression.ReturnType || !valueVar.IsMutable) @@ -135,9 +137,9 @@ public Body SetExpressions(IReadOnlyList expressions) throw new SpanExtensions.EmptyInputIsNotAllowed(); ParsingLineNumber--; var lastExpression = Expressions[^1]; - var isLastExpressionReturn = lastExpression.GetType().Name == Base.Return; - if (Method.ReturnType.Name != Base.None && (isLastExpressionReturn || IsMethodReturn()) && - Method.Name != Base.Run && Method.Name != From && !ChildHasMatchingMethodReturnType( + var isLastExpressionReturn = lastExpression.GetType().Name == "Return"; + if (Method.ReturnType.Name != Type.None && (isLastExpressionReturn || IsMethodReturn()) && + Method.Name != Run && Method.Name != From && !ChildHasMatchingMethodReturnType( Parent == null ? Method.ReturnType : Parent.ReturnType, lastExpression)) @@ -160,26 +162,22 @@ private bool IsMethodReturn() => /// private static bool ChildHasMatchingMethodReturnType(Type parentType, Expression lastExpression) => - lastExpression.GetType().Name == Base.Declaration && parentType.Name == Base.None || + lastExpression.GetType().Name == Declaration && parentType.IsNone || lastExpression.ReturnType.IsError || lastExpression.ReturnType.IsSameOrCanBeUsedAs(parentType) || // Allow automatically converting an item to a list if the method requires a list parentType.IsIterator && parentType.GetListImplementationType(lastExpression.ReturnType) == parentType; + internal const string Declaration = nameof(Declaration); + internal const string MutableReassignment = nameof(MutableReassignment); + public sealed class ChildBodyReturnTypeMustMatchMethod(Body body, Expression lastExpression) - : ParsingFailed(body, - $"Last expression { - lastExpression - } return type: { - lastExpression.ReturnType - } is not matching with expected method return type:" + $" { - (body.Parent == null - ? body.Method.ReturnType - : body.Parent.ReturnType) - } in method line: { - body.ParsingLineNumber - }"); + : ParsingFailed(body, $"Last expression {lastExpression} return type: " + + $"{lastExpression.ReturnType} is not matching with expected method return type: " + + (body.Parent == null + ? body.Method.ReturnType + : body.Parent.ReturnType) + " in method line: " + body.ParsingLineNumber); public sealed class ReturnAsLastExpressionIsNotNeeded(Body body) : ParsingFailed(body); public List? Variables { get; private set; } @@ -188,9 +186,9 @@ public Body AddVariable(string name, Expression value, bool isMutable) { if (name.IsKeyword()) throw new NamedType.CannotUseKeywordsAsName(name); - if (!name.Length.IsWithinLimit()) + if (!name.Length.IsNameLengthWithinLimit()) throw new NamedType.NameLengthIsNotWithinTheAllowedLimit(name); - if (!value.ToString().StartsWith(Base.Error, StringComparison.InvariantCulture)) + if (!value.ToString().StartsWith(Type.Error, StringComparison.InvariantCulture)) CheckForNameWithDifferentTypeUsage(name, value); if (FindVariable(name.AsSpan(), name != Type.IndexLowercase && name != Type.ValueLowercase) is not null) @@ -202,7 +200,7 @@ public Body AddVariable(string name, Expression value, bool isMutable) private void CheckForNameWithDifferentTypeUsage(string name, Expression value) { var nameType = value.ReturnType.TryGetType(name.MakeFirstLetterUppercase()); - if (nameType != null && nameType != value.ReturnType && nameType.Name != Base.Error) + if (nameType != null && nameType != value.ReturnType && nameType.Name != Type.Error) throw new VariableNameCannotHaveDifferentTypeNameThanValue(this, name, value.ReturnType.Name); } @@ -238,7 +236,7 @@ public void CheckIfWeCouldUpdateMutableParameterOrVariable(Type contextType, str } public sealed class IdentifierNotFound(Body body, string name) : ParsingFailed(body, - name + ", Variables in scope: " + body.GetAllVariables().ToWordList()); + name + ", Variables in scope: " + string.Join(", ", body.GetAllVariables())); private List GetAllVariables() { @@ -261,6 +259,24 @@ private List GetAllVariables() public override bool IsConstant => Expressions.All(e => e.IsConstant); public override string ToString() => string.Join(Environment.NewLine, Expressions); + + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || + (other is Body b && Expressions.Count == b.Expressions.Count && ExpressionsEqual(b)); + + private bool ExpressionsEqual(Body other) + { + for (var i = 0; i < Expressions.Count; i++) + if (!Expressions[i].Equals(other.Expressions[i])) + return false; //ncrunch: no coverage + return true; + } + + public override int GetHashCode() => + Expressions.Count > 0 //ncrunch: no coverage + ? Expressions[0].GetHashCode() ^ Expressions.Count + : 0; + public string GetLine(int lineNumber) => Method.lines[lineNumber]; public Body? FindCurrentChild() diff --git a/Strict.Language/Context.cs b/Strict.Language/Context.cs index 41ed503a..ecca061e 100644 --- a/Strict.Language/Context.cs +++ b/Strict.Language/Context.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Runtime.CompilerServices; using static Strict.Language.NamedType; namespace Strict.Language; @@ -25,7 +24,7 @@ protected Context(Context? parent, string name throw new NameMustBeAWordWithoutAnySpecialCharactersOrNumbers(name); if (this is Package && !name.IsAlphaNumericWithAllowedSpecialCharacters()) throw new PackageNameMustBeAWordWithoutSpecialCharacters(name); - if (isNotGeneric && !string.IsNullOrEmpty(name) && !name.Length.IsWithinLimit() && + if (isNotGeneric && !string.IsNullOrEmpty(name) && !name.Length.IsNameLengthWithinLimit() && !name.IsOperatorOrAllowedMethodName()) throw new NameLengthIsNotWithinTheAllowedLimit(name); Parent = parent!; @@ -35,9 +34,9 @@ protected Context(Context? parent, string name this.callerLineNumber = callerLineNumber; this.callerMemberName = callerMemberName; #endif - FullName = string.IsNullOrEmpty(parent?.Name) || parent.Name is nameof(Base) + FullName = string.IsNullOrEmpty(parent?.Name) ? name - : parent + "." + name; + : parent.FullName + ParentSeparator + name; } #if DEBUG @@ -45,6 +44,7 @@ protected Context(Context? parent, string name protected readonly int callerLineNumber; protected readonly string callerMemberName; #endif + public const char ParentSeparator = '/'; private static bool IsNotMethodOrPackageAndNotConflictingType(Context context, Context parent, string name) @@ -59,25 +59,27 @@ public sealed class NameMustBeAWordWithoutAnySpecialCharactersOrNumbers(string n : Exception(name); public sealed class PackageNameMustBeAWordWithoutSpecialCharacters(string name) : Exception( - "Name " + name + - " ;Allowed characters: Alphabets, Numbers or '-' in the middle or end of the name"); + "Name " + name + "; Must start with a letter, then only allowed characters are: Letters " + + "(A-z), Numbers (0-9) or '-'. Do not use '.', '_', spaces or any other special characters."); public Context Parent { get; } public string Name { get; } public string FullName { get; } + public abstract Type? FindType(string name, Context? searchingFrom = null); - // ReSharper disable once InconsistentlySynchronizedField public Type GetType(string name) => - TryGetType(name) ?? throw new TypeNotFound(name, FullName, types.Keys.ToWordList()); + TryGetType(name) ?? throw new TypeNotFound(name, this); internal Type? TryGetType(string name) { lock (types) { + FindTypeCount++; if (types.TryGetValue(name, out var type)) return type; var result = GuessTypeFromName(); - types[name] = result; + if (result != null) + types[name] = result; return result; } @@ -98,6 +100,7 @@ public Type GetType(string name) => } private readonly IDictionary types = new Dictionary(); + internal static int FindTypeCount; public sealed class ListPrefixIsNotAllowedUseImplementationTypeNameInPlural(string typeName) : Exception($"List should not be used as prefix for { @@ -112,22 +115,22 @@ public sealed class ListPrefixIsNotAllowedUseImplementationTypeNameInPlural(stri private Type? TryGetTypeFromPluralNameAsListWithSingularName(string name) { var singularName = name[..^1]; - if (singularName == Base.Generic) - return GetType(Base.List); + if (singularName == Type.GenericUppercase) + return GetType(Type.List); var elementType = FindFullType(singularName) ?? FindType(singularName, this); if (elementType != null) return GetListImplementationType(elementType); return FindFullType(name) ?? FindType(name, this); } - private const string GenericImplementationPostfix = "(" + Base.Generic + ")"; + private const string GenericImplementationPostfix = "(" + Type.GenericUppercase + ")"; private Type GetGenericTypeWithArguments(string name) { var mainType = GetType(name[..name.IndexOf('(')]); var rest = name[(mainType.Name.Length + 1)..^1]; var arguments = rest.Split(',', StringSplitOptions.TrimEntries); - if (rest.Contains("Generic")) + if (rest.Contains(Type.GenericUppercase)) { var namedTypes = GetNamedTypes(mainType, arguments); return mainType.Package.FindDirectType(mainType.GetImplementationName(namedTypes)) ?? @@ -154,18 +157,18 @@ private Type[] GetArgumentTypes(IReadOnlyList argumentTypeNames) } public sealed class TypeArgumentsCountDoesNotMatchGenericType(Type mainType, - IReadOnlyCollection typeArguments) : Exception("The generic type " + mainType + - " needs these type arguments: " + mainType.GetGenericTypeArguments().ToBrackets() + + IReadOnlyList typeArguments) : Exception("The generic type " + mainType + + " needs these type arguments: " + mainType.GetGenericTypeArguments().ToList().ToBrackets() + ", this does not match provided types: " + typeArguments.ToBrackets()); public GenericTypeImplementation GetListImplementationType(Type implementation) => - GetType(Base.List).GetGenericImplementation(implementation); + GetType(Type.List).GetGenericImplementation(implementation); public GenericTypeImplementation GetDictionaryImplementationType(Type keyType, Type valueType) => - GetType(Base.Dictionary).GetGenericImplementation(keyType, valueType); + GetType(Type.Dictionary).GetGenericImplementation(keyType, valueType); private Type? FindFullType(string name) => - name.Contains('.') + name.Contains(ParentSeparator) ? name == FullName ? this as Type : GetPackage()?.FindFullType(name) @@ -173,7 +176,16 @@ public GenericTypeImplementation GetDictionaryImplementationType(Type keyType, T public override string ToString() => FullName; - [MethodImpl(MethodImplOptions.AggressiveInlining)] + //ncrunch: no coverage start + public string ToDebugString() => +#if DEBUG + FullName + " Parent+" + Parent + + ", created from " + callerMemberName + " in " + callerFilePath + ":line " + callerLineNumber; +#else + FullName + " Parent+" + Parent; +#endif + //ncrunch: no coverage end + public Package? GetPackage() => this is Package package // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract @@ -182,8 +194,17 @@ this is Package package : package : Parent.GetPackage(); - public sealed class TypeNotFound(string typeName, string contextFullName, string contextTypes) - : Exception($"{typeName} not found in {contextFullName}, available types: " + contextTypes); - - public abstract Type? FindType(string name, Context? searchingFrom = null); + public sealed class TypeNotFound(string typeName, Context context) + : Exception($"{typeName} not found in\n" + WriteContextTypes(context)) + { + private static string WriteContextTypes(Context context) + { + var result = context.GetType().Name + " " + context.FullName + ", " + + "available " + "types: " + string.Join(", ", context.types.Keys); + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (context.Parent != null && context.Parent.Name != string.Empty) + result += "\n\tParent " + WriteContextTypes(context.Parent); + return result; + } + } } \ No newline at end of file diff --git a/Strict.Language/EqualsExtensions.cs b/Strict.Language/EqualsExtensions.cs deleted file mode 100644 index 0332afec..00000000 --- a/Strict.Language/EqualsExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections; -using System.Globalization; - -namespace Strict.Language; - -/// -/// Comparing C# object value equality sucks we have to do a lot of work to compare two instances -/// like in out-of-the-box Strict. Used most importantly for Value expressions and ValueInstance. -/// -public static class EqualsExtensions -{ - public static bool AreEqual(object? value, object? other) - { - if (ReferenceEquals(value, other)) - return true; - if (value is IList valueList && other is IList otherValueList) - return AreListsEqual(valueList, otherValueList); - if (value is IDictionary valueDict && other is IDictionary otherValueDict) - return AreDictionariesEqual(valueDict, otherValueDict); - if (IsNumeric(value) && IsNumeric(other)) - return NumberToDouble(value) == NumberToDouble(other); - return value?.Equals(other) ?? false; - } - - private static bool AreListsEqual(IList left, IList right) - { - if (left.Count != right.Count) - return false; - for (var i = 0; i < left.Count; i++) - if (!AreEqual(left[i], right[i])) - return false; - return true; - } - - private static bool AreDictionariesEqual(IDictionary left, IDictionary right) - { - if (left.Count != right.Count) - return false; - foreach (DictionaryEntry entry in left) - if (!right.Contains(entry.Key) || !AreEqual(entry.Value, right[entry.Key])) - return false; - return true; - } - - public static bool IsNumeric(object? value) => - value is sbyte or byte or short or ushort or int or uint or long or ulong or float or double - or decimal; - - public static double NumberToDouble(object? n) => Convert.ToDouble(n, CultureInfo.InvariantCulture); -} \ No newline at end of file diff --git a/Strict.Language/Expression.cs b/Strict.Language/Expression.cs index 93bbd0bd..f9a9357f 100644 --- a/Strict.Language/Expression.cs +++ b/Strict.Language/Expression.cs @@ -12,6 +12,7 @@ public abstract class Expression(Type returnType, int lineNumber = 0, bool isMut public Type ReturnType { get; } = returnType; public int LineNumber { get; } = lineNumber; public bool IsMutable { get; } = isMutable; + public abstract bool IsConstant { get; } /// /// By default, all expressions should be immutable in Strict. However, many times some part of /// the code will actually change something, thus making that expression AND anything that calls @@ -32,17 +33,13 @@ public bool ContainsAnythingMutable return false; } } - public abstract bool IsConstant { get; } - - public virtual bool Equals(Expression? other) => - !ReferenceEquals(null, other) && - (ReferenceEquals(this, other) || other.ToString() == ToString()); public override bool Equals(object? obj) => !ReferenceEquals(null, obj) && (ReferenceEquals(this, obj) || obj.GetType() == GetType() && Equals((Expression)obj)); - public override int GetHashCode() => ToString().GetHashCode(); + public abstract bool Equals(Expression? other); + public abstract override int GetHashCode(); public override string ToString() => base.ToString() + " " + ReturnType; protected static string IndentExpression(Expression expression) => diff --git a/Strict.Language/GenericType.cs b/Strict.Language/GenericType.cs index 552675c7..e567432a 100644 --- a/Strict.Language/GenericType.cs +++ b/Strict.Language/GenericType.cs @@ -11,7 +11,7 @@ public GenericType(Type generic, IReadOnlyList genericImplementations base(generic.Package, new TypeLines(generic.GetImplementationName(genericImplementations), HasWithSpaceAtEnd + generic.Name)) { - CreatedBy = "Generic: " + generic + ", GenericImplementations: " + genericImplementations.ToWordList() + + CreatedBy = "Generic: " + generic + ", GenericImplementations: " + string.Join(", ", genericImplementations) + ", " + CreatedBy; Generic = generic; GenericImplementations = genericImplementations; diff --git a/Strict.Language/GenericTypeImplementation.cs b/Strict.Language/GenericTypeImplementation.cs index f3e3d984..207839db 100644 --- a/Strict.Language/GenericTypeImplementation.cs +++ b/Strict.Language/GenericTypeImplementation.cs @@ -6,17 +6,25 @@ public GenericTypeImplementation(Type generic, IReadOnlyList implementatio generic.Package, new TypeLines(generic.GetImplementationName(implementationTypes), CreateHasLines(generic, implementationTypes))) { - CreatedBy = "Generic: " + generic + ", Implementations: " + implementationTypes.ToWordList() + + CreatedBy = "Generic: " + generic + ", Implementations: " + string.Join(", ", implementationTypes) + ", " + CreatedBy; Generic = generic; ImplementationTypes = implementationTypes; ImplementMembers(); ImplementMethods(); + if (Generic.IsMutable) + typeKind = TypeKind.Mutable; + if (Generic.IsError) + typeKind = TypeKind.Error; + if (Generic.IsList) + typeKind = TypeKind.List; + if (Generic.IsDictionary) + typeKind = TypeKind.Dictionary; } private static string[] CreateHasLines(Type generic, IReadOnlyList implementationTypes) => generic.IsMutable && implementationTypes[0].IsGeneric - ? [HasWithSpaceAtEnd + generic.Name, HasWithSpaceAtEnd + Base.Generic] + ? [HasWithSpaceAtEnd + generic.Name, HasWithSpaceAtEnd + GenericUppercase] : [HasWithSpaceAtEnd + generic.Name]; public Type Generic { get; } @@ -27,7 +35,7 @@ private void ImplementMembers() var implementationTypeIndex = 0; foreach (var member in Generic.Members) members.Add((member.Type.IsGeneric || member.Type is GenericType) && - member.Type.Name != Base.Iterator + member.Type.Name != Iterator ? member.CloneWithImplementation(GetImplementedMemberType(member.Type, ref implementationTypeIndex)) : member); @@ -35,13 +43,10 @@ private void ImplementMembers() private Type GetImplementedMemberType(Type memberType, ref int implementationTypeIndex) { - if (memberType is GenericType - { - Generic.Name: Base.List, GenericImplementations.Count: > 1 - } genericType) - return genericType.Generic.GetGenericImplementation( - genericType.Generic.GetGenericImplementation(ImplementationTypes[0])); - return memberType.Name == Base.List + if (memberType is GenericType { Generic.Name: List, GenericImplementations.Count: > 1 } generic) + return generic.Generic.GetGenericImplementation( + generic.Generic.GetGenericImplementation(ImplementationTypes[0])); + return memberType.IsList ? this : ImplementationTypes[implementationTypeIndex++]; } diff --git a/Strict.Language/GitHubStrictDownloader.cs b/Strict.Language/GitHubStrictDownloader.cs new file mode 100644 index 00000000..ad773dd8 --- /dev/null +++ b/Strict.Language/GitHubStrictDownloader.cs @@ -0,0 +1,52 @@ +using System.Net.Http.Headers; +using System.Text.Json; + +namespace Strict.Language; + +//ncrunch: no coverage start +public sealed class GitHubStrictDownloader : IDisposable +{ + public GitHubStrictDownloader(string owner, string repoNameAndFolders) + { + http = new HttpClient(); + http.DefaultRequestHeaders.UserAgent.Add( + new ProductInfoHeaderValue(nameof(GitHubStrictDownloader), "1.0")); + this.owner = owner; + this.repoNameAndFolders = repoNameAndFolders; + } + + private readonly HttpClient http; + private readonly string owner; + private readonly string repoNameAndFolders; + + public async Task DownloadFiles(string outputDirectory, CancellationToken token = default) + { + var apiUrl = "https://api.github.com/repos/" + owner + "/" + repoNameAndFolders + + "/contents?ref=master"; + using var response = await http.GetAsync(apiUrl, token).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(token).ConfigureAwait(false); + var items = JsonSerializer.Deserialize>(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; + foreach (var item in items) + if (item is { Type: "file", Name: not null, DownloadUrl: not null } && + item.Name.EndsWith(Type.Extension, StringComparison.OrdinalIgnoreCase)) + { + var localPath = Path.Combine(outputDirectory, item.Name); + using var fileResponse = await http.GetAsync(item.DownloadUrl, token).ConfigureAwait(false); + fileResponse.EnsureSuccessStatusCode(); + await using var fileStream = File.Create(localPath); + await fileResponse.Content.CopyToAsync(fileStream, token).ConfigureAwait(false); + } + } + + private sealed class ContentItem + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + public string? Name { get; set; } + public string? Type { get; set; } + public string? DownloadUrl { get; set; } + } + + public void Dispose() => http.Dispose(); +} \ No newline at end of file diff --git a/Strict.Language/LogAttribute.cs b/Strict.Language/LogAttribute.cs index e319fabc..85cfeb8d 100644 --- a/Strict.Language/LogAttribute.cs +++ b/Strict.Language/LogAttribute.cs @@ -24,15 +24,11 @@ public void Init(object instance, MethodBase method, object[] args) Console.WriteLine($"[{nameof(Strict)}] Body.Parse {methodValue}, Lines={rangeValue}"); } else - Console.WriteLine($"[{ - nameof(Strict) - }] { - method.DeclaringType?.Name - }{ + Console.WriteLine($"[{nameof(Strict)}] {method.DeclaringType?.Name}"+ (method.Name == ".ctor" ? "" - : "." + method.Name) - }({ + : "." + method.Name)+ + $"({ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract string.Join(", ", args.Select(a => a != null ? a.ToString() diff --git a/Strict.Language/Member.cs b/Strict.Language/Member.cs index 21d61df4..b797af55 100644 --- a/Strict.Language/Member.cs +++ b/Strict.Language/Member.cs @@ -44,7 +44,7 @@ public void ParseConstraints(ExpressionParser parser, string[] constraintsText) { expressions[index] = parser.ParseExpression( new Body(new Method(Type, 0, parser, [ConstraintsBody])), constraintsText[index]); - if (expressions[index].ReturnType.Name != Base.Boolean) + if (!expressions[index].ReturnType.IsBoolean) throw new InvalidConstraintExpression(Type, Name, constraintsText[index]); } Constraints = expressions; diff --git a/Strict.Language/Method.cs b/Strict.Language/Method.cs index 1b3e4ac9..d8410f89 100644 --- a/Strict.Language/Method.cs +++ b/Strict.Language/Method.cs @@ -83,29 +83,33 @@ private static string GetName(ReadOnlySpan firstLine) private Type ParseReturnType(Context type, string returnTypeText) { - if (returnTypeText == Base.Any) + if (returnTypeText == Type.Any) throw new MethodReturnTypeAsAnyIsNotAllowed(this, returnTypeText); var hasMultipleReturnTypes = returnTypeText.Contains(" or ", StringComparison.Ordinal); return hasMultipleReturnTypes - ? ParseMultipleReturnTypes(returnTypeText) + ? ParseMultipleReturnTypes(type, returnTypeText) : type.GetType(returnTypeText); } - private Type ParseMultipleReturnTypes(string typeNames) => - new OneOfType(Type, typeNames.Split(" or ", StringSplitOptions.TrimEntries). - Select(typeName => Type.GetType(typeName)).ToList()); + private Type ParseMultipleReturnTypes(Context type, string typeNames) + { + var types = typeNames.Split(" or ", StringSplitOptions.TrimEntries). + Select(typeName => Type.GetType(typeName)).ToList(); + var typeName = string.Join("Or", types.Select(t => t.Name)); + return type.FindType(typeName) ?? new OneOfType(Type, types); + } private Type GetEmptyReturnType(Type type) => Name is From ? type - : type.GetType(Base.None); + : type.GetType(Type.None); public sealed class MethodReturnTypeAsAnyIsNotAllowed(Method method, string name) : ParsingFailed(method.Type, 0, name); private static bool IsMethodGeneric(ReadOnlySpan headerLine) => - headerLine.Contains(Base.Generic, StringComparison.Ordinal) || - headerLine.Contains(Base.Generic.MakeFirstLetterLowercase(), StringComparison.Ordinal); + headerLine.Contains(Type.GenericUppercase, StringComparison.Ordinal) || + headerLine.Contains(Type.GenericLowercase, StringComparison.Ordinal); public bool IsGeneric { get; } @@ -140,7 +144,7 @@ public sealed class ParametersMustStartWithLowerCase(Method method, string messa : ParsingFailed(method.Type, 0, message, method.Name); private static bool IsParameterTypeAny(string nameAndTypeString) => - nameAndTypeString == Type.AnyLowercase || nameAndTypeString.Contains(" " + Base.Any); + nameAndTypeString == Type.AnyLowercase || nameAndTypeString.Contains(" " + Type.Any); public sealed class ParametersWithTypeAnyIsNotAllowed(Method method, string name) : ParsingFailed(method.Type, 0, name); @@ -233,7 +237,7 @@ internal Method(Method cloneFrom, GenericTypeImplementation typeWithImplementati private static Type ReplaceWithImplementationOrGenericType(Type type, GenericTypeImplementation typeWithImplementation, int index) { - if (type.Name == Base.Generic) + if (type.Name == Type.GenericUppercase) return typeWithImplementation.ImplementationTypes[index]; if (type is GenericTypeImplementation genericImplementation) { @@ -244,7 +248,7 @@ private static Type ReplaceWithImplementationOrGenericType(Type type, implementationIndex < updatedImplementationTypes.Length; implementationIndex++) { var implementationType = genericImplementation.ImplementationTypes[implementationIndex]; - var updatedType = implementationType.Name == Base.Generic + var updatedType = implementationType.Name == Type.GenericUppercase ? typeWithImplementation.ImplementationTypes[index] : implementationType == typeWithImplementation.Generic ? typeWithImplementation @@ -269,14 +273,25 @@ public Expression ParseLine(Body body, string currentLine) Tests.Add(expression); // Checks for obvious recursive calls with same arguments at the last line (non-test method), // this won't catch most recursive calls, see Executor for most other cases. - else if (currentLine.Contains(body.Method.Name + - body.Method.parameters.Select(p => p.Name).ToBrackets()) && - expression.GetType().Name == "MethodCall" && - body.ParsingLineNumber == body.Method.Tests.Count + 1 && currentLine != "\tRun") + else if (expression.GetType().Name == "MethodCall" && + body.ParsingLineNumber == body.Method.Tests.Count + 1 && currentLine != "\tRun" && + currentLine.Contains(body.Method.GetNameWithParameters())) throw new RecursiveCallCausesStackOverflow(body); return expression; } + private string GetNameWithParameters() + { + var result = ""; + foreach (var param in parameters) + result += (result == "" + ? "" + : ", ") + param.Type.Name; + if (result.Length > 0) + result = "(" + result + ")"; + return Name + result; + } + public sealed class RecursiveCallCausesStackOverflow(Body body) : ParsingFailed(body); private static bool IsTestExpression(Body body, string currentLine, Expression expression) => @@ -296,6 +311,7 @@ public List ParseListArguments(Body body, ReadOnlySpan text) = Parser.ParseListArguments(body, text); public const string From = "from"; + public const string Run = nameof(Run); /// /// Skips the first method declaration line, then counts, and removes the tabs from each line. @@ -375,21 +391,19 @@ public Expression GetBodyAndParseIfNeeded(bool parseTestsOnlyForGeneric = false) return ParseTestsOnlyForGeneric(); if (methodBody.Expressions.Count > 0) return !parseTestsOnlyForGeneric && methodBody.Expressions.Any(expression => - expression.GetType().Name == "PlaceholderExpression") + expression.GetType().Name == nameof(PlaceholderExpression)) ? methodBody.Parse() : methodBody.Expressions.Count == 1 ? methodBody.Expressions[0] : methodBody; var expression = methodBody.Parse(); - if (expression.GetType().Name == Base.Declaration) + if (expression.GetType().Name == Body.Declaration) throw new DeclarationIsNeverUsedAndMustBeRemoved(Type, TypeLineNumber, expression); if (methodBody.Variables != null) foreach (var variable in methodBody.Variables) if (variable is { IsMutable: true, InitialValue.IsConstant: true } && !Parser.IsVariableMutated(methodBody, variable.Name)) throw new MutableUsesConstantValue(methodBody, variable.Name, variable.InitialValue); - if (Tests.Count < 1 && !IsTestPackage()) - throw new MethodMustHaveAtLeastOneTest(Type, Name, TypeLineNumber); return BodyParsed?.Invoke(expression) ?? expression; } @@ -422,8 +436,6 @@ private Expression ParseTestsOnlyForGeneric() expressions.Add(expression); } } - if (Tests.Count < 1 && !IsTestPackage()) - throw new MethodMustHaveAtLeastOneTest(Type, Name, TypeLineNumber); //ncrunch: no coverage expressions.Add(new PlaceholderExpression(ReturnType)); methodBody.SetExpressions(expressions); return methodBody; @@ -444,17 +456,20 @@ private static bool IsControlFlowLine(string line) => line.StartsWith("\t\t", StringComparison.Ordinal); private static bool IsStandaloneInlineTestExpression(Expression expression) => - expression.ReturnType.Name == Base.Boolean && + expression.ReturnType.IsBoolean && expression.GetType().Name is not "If" && - expression.GetType().Name is not Base.Return && - expression.GetType().Name is not Base.Declaration && - expression.GetType().Name is not Base.MutableReassignment; + expression.GetType().Name is not "Return" && + expression.GetType().Name is not Body.Declaration && + expression.GetType().Name is not Body.MutableReassignment; - private sealed class PlaceholderExpression(Type returnType) - : Expression(returnType) + internal sealed class PlaceholderExpression(Type returnType) : Expression(returnType) { public override bool IsConstant => true; //ncrunch: no coverage public override string ToString() => ReturnType.Name; + public override bool Equals(Expression? other) => + ReferenceEquals(this, other) || //ncrunch: no coverage + (other is PlaceholderExpression p && ReturnType == p.ReturnType); + public override int GetHashCode() => ReturnType.GetHashCode(); //ncrunch: no coverage } public sealed class DeclarationIsNeverUsedAndMustBeRemoved(Type type, int lineNumber, @@ -473,26 +488,22 @@ internal void SetBodySingleExpression(Expression expression) => : [expression]); public event Func? BodyParsed; - private bool IsTestPackage() => Type.Package.Name == "TestPackage" || Name == "Run"; - - public sealed class MethodMustHaveAtLeastOneTest(Type type, string name, int typeLineNumber) - : ParsingFailed(type, typeLineNumber, name); - public class CannotCallBodyOnTraitMethod(Type type, string name) : Exception(type + "." + name); public override string ToString() => - Name + parameters.ToBrackets() + (ReturnType.Name == Base.None + Name + parameters.ToBrackets() + (ReturnType.IsNone ? "" : " " + ReturnType.Name); public bool HasEqualSignature(Method method) => Name == method.Name && Parameters.Count == method.Parameters.Count && - (ReturnType == method.ReturnType || method.ReturnType.Name == Base.Generic || - ReturnType.Name == Base.Generic) && HasSameParameterTypes(method); + (ReturnType == method.ReturnType || method.ReturnType.Name == Type.GenericUppercase || + ReturnType.Name == Type.GenericUppercase) && HasSameParameterTypes(method); private bool HasSameParameterTypes(Method method) => !method.Parameters.Where((parameter, index) => - parameter.Type.Name != Base.Generic && Parameters[index].Type != parameter.Type).Any(); + parameter.Type.Name != Type.GenericUppercase && + Parameters[index].Type != parameter.Type).Any(); public int GetParameterUsageCount(string parameterName) => lines.Count(l => l.Contains(" " + parameterName) || l.Contains("(" + parameterName) || diff --git a/Strict.Language/NamedType.cs b/Strict.Language/NamedType.cs index 4f434858..3627ef48 100644 --- a/Strict.Language/NamedType.cs +++ b/Strict.Language/NamedType.cs @@ -27,13 +27,13 @@ protected NamedType(Context definedIn, ReadOnlySpan nameAndType, if (!Name.IsWord()) throw new Context.NameMustBeAWordWithoutAnySpecialCharactersOrNumbers(Name); } - if (!Name.Length.IsWithinLimit()) + if (!Name.Length.IsNameLengthWithinLimit()) throw new NameLengthIsNotWithinTheAllowedLimit(Name); } public sealed class CannotUseKeywordsAsName(string name) : Exception(name + " is a keyword and cannot be used as a identifier name. Keywords List: " + - Keyword.GetAllKeywords.ToWordList()); + string.Join(", ", Keyword.GetAllKeywords)); /// /// Most things should NOT be mutable, this is mostly for optimizations like going through for loops @@ -49,15 +49,9 @@ public sealed class CannotUseKeywordsAsName(string name) : Exception(name + public sealed class AssignmentWithInitializerTypeShouldNotHaveNameWithType(string name) : Exception(name); - public sealed class NameLengthIsNotWithinTheAllowedLimit(string name) : Exception($"Name { - name - } length is { - name.Length - } but allowed limit is between { - Limit.NameMinLimit - } and { - Limit.NameMaxLimit - }"); + public sealed class NameLengthIsNotWithinTheAllowedLimit(string name) : Exception( + $"Name {name} length is {name.Length} but allowed limit is between " + + $"{Limit.NameMinLimit} and {Limit.NameMaxLimit} characters."); public string Name { get; } public Type Type { get; protected set; } diff --git a/Strict.Language/NumberExtensions.cs b/Strict.Language/NumberExtensions.cs index f0941ba9..982850aa 100644 --- a/Strict.Language/NumberExtensions.cs +++ b/Strict.Language/NumberExtensions.cs @@ -2,6 +2,6 @@ public static class NumberExtensions { - public static bool IsWithinLimit(this int length) => + public static bool IsNameLengthWithinLimit(this int length) => length is >= Limit.NameMinLimit and <= Limit.NameMaxLimit; } \ No newline at end of file diff --git a/Strict.Language/OneOfType.cs b/Strict.Language/OneOfType.cs index e788432b..108e1122 100644 --- a/Strict.Language/OneOfType.cs +++ b/Strict.Language/OneOfType.cs @@ -1,8 +1,8 @@ namespace Strict.Language; public sealed class OneOfType(Type definedInType, IReadOnlyList types) : Type( - definedInType.Package, - new TypeLines(string.Join("Or", types.Select(t => t.Name)), GetOneOfTypeLines(types))) + definedInType.Package, new TypeLines(string.Join("Or", types.Select(t => t.Name)), + GetOneOfTypeLines(types))) { private static string[] GetOneOfTypeLines(IReadOnlyList types) { diff --git a/Strict.Language/Package.cs b/Strict.Language/Package.cs index 78fb2047..d046ee54 100644 --- a/Strict.Language/Package.cs +++ b/Strict.Language/Package.cs @@ -1,40 +1,75 @@ -using System.Collections; using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Strict.Compiler.Cuda")] + namespace Strict.Language; /// /// In C# or Java called namespace or package as well, in Strict this is any code folder. /// -public class Package : Context, IEnumerable, IDisposable +public class Package : Context, IDisposable { #if DEBUG - public Package(string packagePath, [CallerFilePath] string callerFilePath = "", - [CallerLineNumber] int callerLineNumber = 0, + public Package(string packagePath, Repositories? createdFromRepos = null, + [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, [CallerMemberName] string callerMemberName = "") : this(RootForPackages, packagePath, - // ReSharper disable ExplicitCallerInfoArgument - callerFilePath, callerLineNumber, callerMemberName) { } + createdFromRepos, callerFilePath, callerLineNumber, callerMemberName) { } +#else + public Package(string packagePath, Repositories? createdFromRepos = null) + : this(RootForPackages, packagePath, createdFromRepos) { } +#endif +#if DEBUG + public Package(Package? parentPackage, string packagePath, Repositories? createdFromRepos = null, + [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, + [CallerMemberName] string callerMemberName = "") : base(parentPackage, + Path.GetFileName(packagePath), callerFilePath, callerLineNumber, callerMemberName) #else - public Package(string packagePath) : this(RootForPackages, packagePath) { } + public Package(Package? parentPackage, string packagePath, Repositories? createdFromRepos = null) + : base(parentPackage, Path.GetFileName(packagePath)) +#endif + { + this.createdFromRepos = createdFromRepos; + FolderPath = Path.IsPathRooted(packagePath) + ? packagePath + : null; + if (parentPackage == null) + return; + var existing = parentPackage.children.FirstOrDefault(existing => existing.Name == Name); + if (existing != null) + throw new PackageAlreadyExists(Name, parentPackage, existing); //ncrunch: no coverage + parentPackage.children.Add(this); + } + + public class PackageAlreadyExists(string name, Package parentPackage, Package existing) + : Exception(name + " in " + (parentPackage.Name == "" //ncrunch: no coverage + ? nameof(Root) + : "parent package " + parentPackage) + ", existing package " + existing.Name +#if DEBUG + + ", existing package created by " + existing.callerFilePath + ":" + + existing.callerLineNumber + " from method " + existing.callerMemberName #endif + ); + private static readonly Root RootForPackages = new(); + private readonly Repositories? createdFromRepos; + public string? FolderPath { get; } /// /// Contains all high level s. Just contains the fallback None type (think - /// void), has no parent and just contains all root children packages. Also features a cache of - /// types searched from here so future access is much faster. See green comment here: - /// https://strict.dev/img/FindType2020-07-01.png + /// void), has no parent, and just contains all root children packages. Also features a cache of + /// types searched from here so future access is much faster. See the green comment here: + /// https://strict-lang.org/img/FindType2020-07-01.png /// private sealed class Root : Package { #if DEBUG public Root([CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, - [CallerMemberName] string callerMemberName = "") : base(null, string.Empty, callerFilePath, - callerLineNumber, callerMemberName) => + [CallerMemberName] string callerMemberName = "") : base(null, string.Empty, null, + callerFilePath, callerLineNumber, callerMemberName) => #else public Root() : base(null, string.Empty) => #endif - cachedFoundTypes.Add(Base.None, new Type(this, new TypeLines(Base.None))); + cachedFoundTypes.Add(Type.None, new Type(this, new TypeLines(Type.None))); public override Type? FindType(string name, Context? searchingFrom = null) => cachedFoundTypes.TryGetValue(name, out var previouslyFoundType) @@ -51,47 +86,22 @@ public Root() : base(null, string.Empty) => private readonly Dictionary cachedFoundTypes = new(StringComparer.Ordinal); } - public Package(Package? parentPackage, string packagePath, [CallerFilePath] string callerFilePath = "", - [CallerLineNumber] int callerLineNumber = 0, - [CallerMemberName] string callerMemberName = "") : base(parentPackage, - Path.GetFileName(packagePath), callerFilePath, callerLineNumber, callerMemberName) - { - FolderPath = packagePath; - if (parentPackage == null) - return; - var existing = parentPackage.children.FirstOrDefault(existingPackage => existingPackage.Name == Name); - if (existing != null) - throw new PackageAlreadyExists(Name, parentPackage, existing); //ncrunch: no coverage - parentPackage.children.Add(this); - } - - public class PackageAlreadyExists(string name, Package parentPackage, Package existing) - : Exception(name + " in " + (parentPackage.Name == "" //ncrunch: no coverage - ? nameof(Root) - : "parent package " + parentPackage) -#if DEBUG - + ", existing package created by " + existing.callerFilePath + ":" + - existing.callerLineNumber + " from method " + existing.callerMemberName -#endif - ); - - public string FolderPath { get; } private readonly List children = new(); internal void Add(Type type) => types.Add(type.Name, type); private readonly Dictionary types = new(); public Type? FindFullType(string fullName) { - var parts = fullName.Split('.'); + var parts = fullName.Split(Context.ParentSeparator); if (parts.Length < 2) throw new FullNameMustContainPackageAndTypeNames(); if (IsPrivateName(parts[^1])) throw new PrivateTypesAreOnlyAvailableInItsPackage(); - if (!fullName.StartsWith(ToString() + ".", StringComparison.Ordinal)) + if (!fullName.StartsWith(FullName + Context.ParentSeparator, StringComparison.Ordinal)) return (Parent as Package)?.FindFullType(fullName); - var subName = fullName.Replace(ToString() + ".", ""); - return subName.Contains('.') - ? FindSubPackage(subName.Split('.')[0])?.FindFullType(fullName) + var subName = fullName.Replace(FullName + Context.ParentSeparator, ""); + return subName.Contains(Context.ParentSeparator) + ? FindSubPackage(subName.Split(Context.ParentSeparator)[0])?.FindFullType(fullName) : FindDirectType(subName); } @@ -103,8 +113,8 @@ public sealed class PrivateTypesAreOnlyAvailableInItsPackage : Exception; /// /// The following picture shows the typical search steps and optimizations done. It is different - /// from simple binary searchs or finding types in other languages because in Strict any public - /// type can be used at any place. https://strict.dev/img/FindType2020-07-01.png + /// from simple binary searches or finding types in other languages because in Strict any public + /// type can be used at any place. https://strict-lang.org/img/FindType2020-07-01.png /// public override Type? FindType(string name, Context? searchingFrom = null) { @@ -166,9 +176,17 @@ public void Remove(Type? type) } internal void Remove(Package package) => children.Remove(package); - public IEnumerator GetEnumerator() => new List(types.Values).GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); //ncrunch: no coverage + public IReadOnlyDictionary Types => types; - // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - public void Dispose() => ((Package)Parent)?.Remove(this); + public void Dispose() + { + GC.SuppressFinalize(this); + while (children.Count > 0) + children[0].Dispose(); + foreach (var type in types) + type.Value.Dispose(); + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + ((Package)Parent)?.Remove(this); + createdFromRepos?.Remove(this); + } } \ No newline at end of file diff --git a/Strict.Language/Repositories.cs b/Strict.Language/Repositories.cs index 51a08d51..8900db1b 100644 --- a/Strict.Language/Repositories.cs +++ b/Strict.Language/Repositories.cs @@ -1,4 +1,3 @@ -using System.IO.Compression; using System.Runtime.CompilerServices; using LazyCache; @@ -7,202 +6,137 @@ namespace Strict.Language; /// -/// Loads packages from url (like GitHub) and caches it to disc for the current and subsequent -/// runs. Next time Repositories is created, we will check for outdated cache and delete the zip -/// files to allow redownloading fresh files. All locally cached packages and all types in them -/// are always available for any .strict file in the Editor. If a type is not found, -/// packages.strict.dev is asked if we can get a url (used here to load). +/// Loads packages from url (like GitHub) and caches it to disc for the current and later runs. +/// Next time Repositories is created, we will check for outdated cache and delete the zip files +/// to allow redownloading fresh files. All locally cached packages and all types in them are +/// always available for any .strict file in the Editor. If a type is not found, we check on github /// -/// Everything in here is async, you can easily load many packages in parallel +/// Everything in here is async, you can load many packages in parallel public sealed class Repositories { /// - /// Gets rid of any cached zip files (keeps the actual files for use) older than 1h, which will - /// allow redownloading from GitHub to get any changes, while still staying fast in local runs - /// when there are usually no changes happening. + /// Keeps a cache of loaded repositories for 20 minutes, default CachingService.DefaultCachePolicy /// public Repositories(ExpressionParser parser) { cacheService = new CachingService(); this.parser = parser; - if (Directory.Exists(CacheFolder)) - //ncrunch: no coverage start, rarely happens - foreach (var file in Directory.GetFiles(CacheFolder, "*.zip")) - if (File.GetLastWriteTimeUtc(file) < DateTime.UtcNow.AddHours(-1)) - File.Delete(file); - } //ncrunch: no coverage end + } private readonly IAppCache cacheService; private readonly ExpressionParser parser; - - public async Task LoadFromUrl(Uri packageUrl + public Task LoadStrictPackage(string packageSubfolder = "" #if DEBUG , [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, [CallerMemberName] string callerMemberName = "" #endif - ) - { - var isStrictPackage = packageUrl.AbsoluteUri.StartsWith(StrictPrefixUri.AbsoluteUri, StringComparison.Ordinal); - if (!isStrictPackage && (packageUrl.Host != "github.com" || string.IsNullOrEmpty(packageUrl.AbsolutePath))) - throw new OnlyGithubDotComUrlsAreAllowedForNow(); - var packageName = packageUrl.AbsolutePath.Split('/').Last(); - if (isStrictPackage) - { - var developmentFolder = - StrictDevelopmentFolderPrefix.Replace(nameof(Strict) + ".", packageName); - if (Directory.Exists(developmentFolder)) - return await LoadFromPath(developmentFolder + ) => #if DEBUG - // ReSharper disable ExplicitCallerInfoArgument - , callerFilePath, callerLineNumber, callerMemberName + LoadFromUrl(new Uri(GitHubStrictUri.AbsoluteUri + (packageSubfolder == "" + ? "" + : "/" + packageSubfolder)), callerFilePath, callerLineNumber, callerMemberName); +#else + LoadFromUrl(new Uri(GitHubStrictUri.AbsoluteUri + (packageSubfolder == "" + ? "" + : "/" + packageSubfolder))); #endif - ); - } //ncrunch: no coverage - return await FindOrAddPath(packageUrl, packageName); //ncrunch: no coverage - } - private async Task FindOrAddPath(Uri packageUrl, string packageName) - { //ncrunch: no coverage start - var localPath = Path.Combine(CacheFolder, packageName); - if (!PreviouslyCheckedDirectories.Add(localPath)) - return await LoadFromPath(localPath); - if (!Directory.Exists(localPath)) - localPath = await DownloadAndExtractRepository(packageUrl, packageName); - return await LoadFromPath(localPath); - } //ncrunch: no coverage end - - public Task LoadStrictPackage(string packagePostfixName = nameof(Base) + public async Task LoadFromUrl(Uri packageUrl #if DEBUG , [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, [CallerMemberName] string callerMemberName = "" #endif - ) => - LoadFromUrl(new Uri(StrictPrefixUri.AbsoluteUri + packagePostfixName), callerFilePath, - callerLineNumber, callerMemberName); - - public sealed class OnlyGithubDotComUrlsAreAllowedForNow : Exception; - //ncrunch: no coverage start, only called once per session and only if not on development machine - private static readonly HashSet PreviouslyCheckedDirectories = new(); - - internal static async Task DownloadAndExtractRepository(Uri packageUrl, - string packageName) - { - if (!Directory.Exists(CacheFolder)) - Directory.CreateDirectory(CacheFolder); - var targetPath = Path.Combine(CacheFolder, packageName); - if (Directory.Exists(targetPath) && - File.Exists(Path.Combine(CacheFolder, packageName + ".zip"))) - return targetPath; - await DownloadAndExtract(packageUrl, packageName, targetPath); - return targetPath; - } - - private static async Task DownloadAndExtract(Uri packageUrl, string packageName, - string targetPath) + ) { - var localZip = Path.Combine(CacheFolder, packageName + ".zip"); - using HttpClient client = new(); - await DownloadFile(client, new Uri(packageUrl + "/archive/master.zip"), localZip); - await Task.Run(() => - UnzipInCacheFolderAndMoveToTargetPath(packageName, targetPath, localZip)); - } + var parts = packageUrl.AbsoluteUri.Split('/'); + if (parts.Length < 5 || parts[0] != "https:" || parts[1] != "" || parts[2] != "github.com") + throw new OnlyGithubDotComUrlsAreAllowedForNow(packageUrl.AbsoluteUri); + var organization = parts[3]; + var remaining = parts[4..]; + var packageFullName = string.Join(Context.ParentSeparator.ToString(), remaining); + var localDevelopmentPath = GetLocalDevelopmentPath(organization, packageFullName); + if (Directory.Exists(localDevelopmentPath)) + return await LoadFromPath(packageFullName, localDevelopmentPath +#if DEBUG + , callerFilePath, callerLineNumber, callerMemberName +#endif + ); + //ncrunch: no coverage start + var localCachePath = Path.Combine(CacheFolder, organization, packageFullName); + if (PreviouslyCheckedDirectories.Add(localCachePath) && !Directory.Exists(localCachePath)) + await DownloadRepositoryStrictFiles(localCachePath, organization, packageFullName); + return await LoadFromPath(packageFullName, localCachePath +#if DEBUG + , callerFilePath, callerLineNumber, callerMemberName +#endif + ); + } //ncrunch: no coverage end - private static async Task DownloadFile(HttpClient client, Uri uri, string fileName) - { - await using var stream = await client.GetStreamAsync(uri); - await using var file = new FileStream(fileName, FileMode.CreateNew); - await stream.CopyToAsync(file); - } + public static string GetLocalDevelopmentPath(string organization, string packageFullName) => + DevelopmentBaseFolder + organization + Context.ParentSeparator + packageFullName; - private static void UnzipInCacheFolderAndMoveToTargetPath(string packageName, string targetPath, - string localZip) - { - ZipFile.ExtractToDirectory(localZip, CacheFolder, true); - var masterDirectory = Path.Combine(CacheFolder, packageName + "-master"); - if (!Directory.Exists(masterDirectory)) - throw new NoMasterFolderFoundFromPackage(packageName, localZip); - if (Directory.Exists(targetPath)) - new DirectoryInfo(targetPath).Delete(true); - TryMoveOrCopyWhenDeletionDidNotFullyWork(targetPath, masterDirectory); - } + public sealed class OnlyGithubDotComUrlsAreAllowedForNow(string uri) : Exception(uri + + " is invalid. Valid url: " + GitHubStrictUri + ", it must always start with " + + "https://github.com and only include the organization and repo name, nothing else!"); - public sealed class NoMasterFolderFoundFromPackage(string packageName, string localZip) - : Exception(packageName + ", localZip: " + localZip); + //ncrunch: no coverage start, only called once per session and only if not on development machine + private static readonly HashSet PreviouslyCheckedDirectories = new(); - private static void TryMoveOrCopyWhenDeletionDidNotFullyWork(string targetPath, - string masterDirectory) + internal static async Task DownloadRepositoryStrictFiles(string localCachePath, string org, + string repoNameAndOptionalSubFolders) { - try - { - Directory.Move(masterDirectory, targetPath); - } - catch - { - foreach (var file in Directory.GetFiles(masterDirectory)) - File.Copy(file, Path.Combine(targetPath, Path.GetFileName(file)), true); - } + if (!Directory.Exists(localCachePath)) + Directory.CreateDirectory(localCachePath); + using var downloader = new GitHubStrictDownloader(org, repoNameAndOptionalSubFolders); + await downloader.DownloadFiles(localCachePath); } //ncrunch: no coverage end - public Task LoadFromPath(string packagePath + public Task LoadFromPath(string fullName, string packagePath #if DEBUG , [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, [CallerMemberName] string callerMemberName = "" #endif ) => - cacheService.GetOrAddAsync(packagePath, - _ => CreatePackageFromFiles(packagePath, - // ReSharper disable ExplicitCallerInfoArgument - Directory.GetFiles(packagePath, "*" + Type.Extension), null, callerFilePath, - callerLineNumber, callerMemberName)); + cacheService.GetOrAddAsync(fullName, _ => CreatePackageFromFiles(packagePath, + Directory.GetFiles(packagePath, "*" + Type.Extension) +#if DEBUG + , null, callerFilePath, callerLineNumber, callerMemberName)); +#else + )); +#endif /// /// Initially we need to create just empty types, and then after they all have been created, /// we will fill and load them, otherwise we could not use types within the package context. /// private async Task CreatePackageFromFiles(string packagePath, - IReadOnlyCollection files, + IReadOnlyCollection files, Package? parent = null #if DEBUG - Package? parent = null, [CallerFilePath] string callerFilePath = "", - [CallerLineNumber] int callerLineNumber = 0, [CallerMemberName] string callerMemberName = "") -#else - Package? parent = null) + , [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, + [CallerMemberName] string callerMemberName = "" #endif + ) { // The main folder can be empty, other folders must contain at least one file to create a package if (parent != null && files.Count == 0) return parent; //ncrunch: no coverage #if DEBUG - var folderName = Path.GetFileName(packagePath); var package = parent != null - // ReSharper disable ExplicitCallerInfoArgument - ? new Package(parent, packagePath, callerFilePath, callerLineNumber, callerMemberName) - : new Package(folderName.Contains('.') - ? folderName.Split('.')[1] - : packagePath, callerFilePath, callerLineNumber, callerMemberName); + ? new Package(parent, packagePath, this, callerFilePath, callerLineNumber, callerMemberName) + : new Package(packagePath, this, callerFilePath, callerLineNumber, callerMemberName); #else - var folderName = Path.GetFileName(packagePath); var package = parent != null - ? new Package(parent, packagePath) - : new Package(folderName.Contains('.') - ? folderName.Split('.')[1] - : packagePath); + ? new Package(parent, packagePath, this) + : new Package(packagePath, this); #endif - if (package.Name == nameof(Strict) && files.Count > 0) - throw new NoFilesAllowedInStrictFolderNeedsToBeInASubFolder(files); //ncrunch: no coverage + loadedPackages.Add(package); var types = GetTypes(files, package); foreach (var type in types) type.ParseMembersAndMethods(parser); - await GetSubDirectoriesAndParse(packagePath, package -#if DEBUG - , callerFilePath, callerLineNumber, callerMemberName -#endif - ); return package; } - //ncrunch: no coverage start - public sealed class NoFilesAllowedInStrictFolderNeedsToBeInASubFolder(IEnumerable files) - : Exception(files.ToWordList()); //ncrunch: no coverage end + private readonly List loadedPackages = []; private ICollection GetTypes(IReadOnlyCollection files, Package package) { @@ -212,7 +146,7 @@ private ICollection GetTypes(IReadOnlyCollection files, Package pa { var lines = new TypeLines(Path.GetFileNameWithoutExtension(filePath), File.ReadAllLines(filePath)); - if (lines.Name != Base.Mutable && lines.DependentTypes.Count > 0) + if (lines.Name != Type.Mutable && lines.DependentTypes.Count > 0) filesWithMembers.Add(lines.Name, lines); else types.Add(new Type(package, lines)); @@ -303,50 +237,33 @@ private static ICollection GetTypesFromSortedFiles(ICollection types return types; } - private async Task GetSubDirectoriesAndParse(string packagePath, Package package -#if DEBUG - , string callerFilePath, int callerLineNumber, string callerMemberName -#endif - ) - { - var subDirectories = Directory.GetDirectories(packagePath); - if (subDirectories.Length > 0) - await Task.WhenAll(ParseAllSubFolders(subDirectories, package -#if DEBUG - , callerFilePath, callerLineNumber, callerMemberName -#endif - )); - } + public const string DevelopmentBaseFolder = @"C:\code\GitHub\"; + internal static string CacheFolder => + Path.Combine( //ncrunch: no coverage, only downloaded and cached on non-development machines + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), StrictPackages); + private const string StrictPackages = nameof(StrictPackages); + public const string StrictOrg = "strict-lang"; + public static readonly Uri GitHubStrictUri = + new("https://github.com/" + StrictOrg + "/" + nameof(Strict)); - private List ParseAllSubFolders(IEnumerable subDirectories, Package package -#if DEBUG - , string callerFilePath, int callerLineNumber, string callerMemberName -#endif - ) + /// + /// Called by Package.Dispose + /// + internal void Remove(Package result) { - var tasks = new List(); - foreach (var directory in subDirectories) - if (IsValidCodeDirectory(directory)) - tasks.Add(CreatePackageFromFiles(directory, //ncrunch: no coverage - Directory.GetFiles(directory, "*" + Type.Extension), package -#if DEBUG - , callerFilePath, callerLineNumber, callerMemberName -#endif - )); - return tasks; + cacheService.Remove(result.FullName); + loadedPackages.Remove(result); } - /// - /// In Strict only words are valid directory names = package names, no symbols (like .git, .hg, - /// .vs or _NCrunch) or numbers or dot separators (like Strict.Compiler) are allowed. - /// - private static bool IsValidCodeDirectory(string directory) => - Path.GetFileName(directory).IsWord(); + public bool ContainsPackageNameInCache(string fullName) => + cacheService.TryGetValue>(fullName, out _); - public const string StrictDevelopmentFolderPrefix = @"C:\code\GitHub\strict-lang\Strict."; - private static string CacheFolder => - Path.Combine( //ncrunch: no coverage, only downloaded and cached on non-development machines - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), StrictPackages); - private const string StrictPackages = nameof(StrictPackages); - public static readonly Uri StrictPrefixUri = new("https://github.com/strict-lang/Strict."); + public async Task ToDebugString() => + nameof(Repositories) + + "\nStrict: " + (cacheService.TryGetValue>(nameof(Strict), + out var lazyPackage) + ? (await lazyPackage.Value).ToDebugString() + : "") + + "\nLoadedPackages: " + string.Join("\n ", loadedPackages) + + "\nPreviouslyCheckedDirectories: " + string.Join(", ", PreviouslyCheckedDirectories.ToList()); } \ No newline at end of file diff --git a/Strict.Language/SpanExtensions.cs b/Strict.Language/SpanExtensions.cs index 65d0cf1d..9fca8970 100644 --- a/Strict.Language/SpanExtensions.cs +++ b/Strict.Language/SpanExtensions.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; namespace Strict.Language; @@ -60,19 +60,19 @@ public static bool Compare(this ReadOnlySpan first, ReadOnlySpan sec } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Any(this ReadOnlySpan input, IEnumerable items) + public static bool Any(this ReadOnlySpan input, IReadOnlyList items) { - foreach (var item in items) - if (input.Compare(item.AsSpan())) + for (var i = 0; i < items.Count; i++) + if (input.Compare(items[i].AsSpan())) return true; return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ContainsAnyItem(this ReadOnlySpan input, IEnumerable items) + public static bool ContainsAnyItem(this ReadOnlySpan input, IReadOnlyList items) { - foreach (var item in items) - if (input.IndexOf(item.AsSpan()) >= 0) + for (var i = 0; i < items.Count; i++) + if (input.IndexOf(items[i].AsSpan()) >= 0) return true; return false; } diff --git a/Strict.Language/Strict.Language.csproj b/Strict.Language/Strict.Language.csproj index 984596f5..00b84159 100644 --- a/Strict.Language/Strict.Language.csproj +++ b/Strict.Language/Strict.Language.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/Strict.Language/StringExtensions.cs b/Strict.Language/StringExtensions.cs index 8074b627..b1bb2994 100644 --- a/Strict.Language/StringExtensions.cs +++ b/Strict.Language/StringExtensions.cs @@ -4,56 +4,32 @@ namespace Strict.Language; public static class StringExtensions { - public static string ToBrackets(this IEnumerable list) => + public static string ToBrackets(this IReadOnlyList list) => list.Any() - ? "(" + list.ToWordList() + ")" + ? "(" + string.Join(DefaultSeparator, list) + ")" : ""; - public static string ToWordList(this IEnumerable list, string separator = ", ") => - list is IDictionary dictionary - ? dictionary.DictionaryToWordList(separator) - : string.Join(separator, list); + private const string DefaultSeparator = ", "; - public static string EnumerableToWordList(this IEnumerable values, string separator = ", ", - bool outputTypes = false) => - values switch - { - IDictionary dict => dict.DictionaryToWordList(outputTypes: outputTypes), - IDictionary iDictionary => iDictionary.IDictionaryToWordList(outputTypes: outputTypes), - _ => values as string ?? values.Cast().ToWordList(separator) - }; + public static string ToLines(this IEnumerable lines) => + string.Join(Environment.NewLine, lines); - public static string DictionaryToWordList(this IDictionary list, + public static string DictionaryToWordList(this IReadOnlyDictionary list, string separator = "; ", string keyValueSeparator = "=", bool outputTypes = false) - where TKey : notnull + where Key : notnull { var result = new List(); foreach (var pair in list) result.Add(pair.Key + (outputTypes && pair.Key is not string ? " (" + pair.Key.GetType().Name + ")" : "") + keyValueSeparator + (pair.Value is IEnumerable values - ? values.EnumerableToWordList(outputTypes: outputTypes) + ? values as string ?? string.Join(", ", values.Cast()) : pair.Value + (outputTypes && pair.Value is not string && pair.Value is not int && pair.Value is not double && pair.Value is not bool && pair.Value?.GetType().Name != "ValueInstance" ? " (" + pair.Value?.GetType().Name + ")" : ""))); - return result.ToWordList(separator); - } - - public static string IDictionaryToWordList(this IDictionary list, string separator = "; ", - bool outputTypes = false) - { - var enumerator = list.GetEnumerator(); - using var disposeEnumerator = enumerator as IDisposable; - var result = new List(); - while (enumerator.MoveNext()) - result.Add(enumerator.Key + "=" + (enumerator.Value is IEnumerable values - ? values.EnumerableToWordList(outputTypes: outputTypes) - : enumerator.Value + (outputTypes - ? " (" + enumerator.Value?.GetType().Name + ")" - : ""))); - return result.ToWordList(separator); + return string.Join(separator, result); } public static bool IsWordOrWordWithNumberAtEnd(this ReadOnlySpan text, out int number) @@ -61,8 +37,8 @@ public static bool IsWordOrWordWithNumberAtEnd(this ReadOnlySpan text, out number = -1; for (var index = 0; index < text.Length; index++) if (!char.IsAsciiLetter(text[index])) - return index == text.Length - 1 && int.TryParse(text[index].ToString(), out number) && - number is > 1 and < 10; + return index == text.Length - 1 && char.IsAsciiDigit(text[index]) && + (number = text[index] - '0') is > 1 and < 10; return true; } @@ -97,10 +73,22 @@ public bool IsAlphaNumericWithAllowedSpecialCharacters() } public string MakeFirstLetterUppercase() => - text[..1].ToUpperInvariant() + text[1..]; + text.Length == 0 || char.IsUpper(text[0]) + ? text + : string.Create(text.Length, text, static (span, s) => + { + span[0] = char.ToUpperInvariant(s[0]); + s.AsSpan(1).CopyTo(span[1..]); + }); public string MakeFirstLetterLowercase() => - text[..1].ToLowerInvariant() + text[1..]; + text.Length == 0 || char.IsLower(text[0]) + ? text + : string.Create(text.Length, text, static (span, s) => + { + span[0] = char.ToLowerInvariant(s[0]); + s.AsSpan(1).CopyTo(span[1..]); + }); public string GetTextInsideBrackets() { diff --git a/Strict.Language/Type.cs b/Strict.Language/Type.cs index 726f99c3..6e28a50e 100644 --- a/Strict.Language/Type.cs +++ b/Strict.Language/Type.cs @@ -1,4 +1,6 @@ +#if DEBUG using System.Runtime.CompilerServices; +#endif namespace Strict.Language; @@ -9,6 +11,45 @@ namespace Strict.Language; /// public class Type : Context, IDisposable { + /// + /// Has no implementation and is used for void, empty, or none, which is not valid to assign. + /// + public const string None = nameof(None); + /// + /// Defines all the methods available in any type (everything automatically implements **Any**). + /// These methods don't have to be implemented by any class, they are automatically implemented. + /// + public const string Any = nameof(Any); + /// + /// Most basic type: can only be true or false, any statement must either be None or return a + /// Boolean (anything else is a compiler error). Any statement returning false (like a failing + /// test) will also immediately cause an error at runtime or in the Editor via SCrunch. + /// + public const string Boolean = nameof(Boolean); + /// + /// Can be any floating point or integer number (think byte, short, int, long, float, or double + /// in other languages). Also, it can be a decimal or BigInteger, the compiler can decide and + /// optimize this away into anything that makes sense in the current context. + /// + public const string Number = nameof(Number); + public const string Character = nameof(Character); + public const string HashCode = nameof(HashCode); + public const string Range = nameof(Range); + public const string Text = nameof(Text); + public const string Error = nameof(Error); + public const string ErrorWithValue = nameof(ErrorWithValue); + public const string Iterator = nameof(Iterator); + public const string List = nameof(List); + public const string Logger = nameof(Logger); + public const string App = nameof(App); + public const string System = nameof(System); + public const string File = nameof(File); + public const string Directory = nameof(Directory); + public const string TextWriter = nameof(TextWriter); + public const string TextReader = nameof(TextReader); + public const string Stacktrace = nameof(Stacktrace); + public const string Mutable = nameof(Mutable); + public const string Dictionary = nameof(Dictionary); #if DEBUG public Type(Package package, TypeLines file, [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, @@ -25,17 +66,18 @@ public Type(Package package, TypeLines file) : base(package, file.Name) throw new TypeAlreadyExistsInPackage(Name, package, existingType); package.Add(this); Lines = file.Lines; - IsGeneric = Name == Base.Generic || OneOfFirstThreeLinesContainsGeneric(); + IsGeneric = Name == GenericUppercase || OneOfFirstThreeLinesContainsGeneric(); CreatedBy = "Package: " + package + ", file=" + file; typeMethodFinder = new TypeMethodFinder(this); typeParser = new TypeParser(this, Lines); + typeKind = GetTypeKindFromName(); } public sealed class LinesCountMustNotExceedLimit(Type type, int lineCount) : ParsingFailed(type, lineCount, $"Type {type.Name} has lines count {lineCount} but limit is {Limit.LineCount}"); public sealed class TypeAlreadyExistsInPackage(string name, Package package, Type existingType) - : Exception(name + " in package: " + package + : Exception(name + " in package: " + package + ", existing type : " + existingType #if DEBUG + ", existing type created by " + existingType.callerFilePath + ":" + existingType.callerLineNumber + " from method " + existingType.callerMemberName @@ -55,6 +97,7 @@ public sealed class TypeAlreadyExistsInPackage(string name, Package package, Typ public string CreatedBy { get; protected init; } private readonly TypeMethodFinder typeMethodFinder; private readonly TypeParser typeParser; + internal TypeKind typeKind; private bool OneOfFirstThreeLinesContainsGeneric() { @@ -65,10 +108,29 @@ private bool OneOfFirstThreeLinesContainsGeneric() return false; } + private TypeKind GetTypeKindFromName() => + Name switch + { + None => TypeKind.None, + Boolean => TypeKind.Boolean, + Number => TypeKind.Number, + Text => TypeKind.Text, + Character => TypeKind.Character, + List => TypeKind.List, + Dictionary => TypeKind.Dictionary, + Error => TypeKind.Error, + ErrorWithValue => TypeKind.Error, + Iterator => TypeKind.Iterator, + Mutable => TypeKind.Mutable, + nameof(Name) => TypeKind.Text, + Any => TypeKind.Any, + _ => TypeKind.Unknown + }; + private static bool HasGenericMember(string line) => (line.StartsWith(HasWithSpaceAtEnd, StringComparison.Ordinal) || line.StartsWith(MutableWithSpaceAtEnd, StringComparison.Ordinal)) && - (line.Contains(Base.Generic, StringComparison.Ordinal) || + (line.Contains(GenericUppercase, StringComparison.Ordinal) || line.Contains(GenericLowercase, StringComparison.Ordinal)); public const string HasWithSpaceAtEnd = Keyword.Has + " "; @@ -76,7 +138,7 @@ private static bool HasGenericMember(string line) => public const string ConstantWithSpaceAtEnd = Keyword.Constant + " "; private static bool HasGenericMethodHeader(string line) => - line.Contains(Base.Generic, StringComparison.Ordinal) || + line.Contains(GenericUppercase, StringComparison.Ordinal) || line.Contains(GenericLowercase, StringComparison.Ordinal); /// @@ -88,6 +150,7 @@ public Type ParseMembersAndMethods(ExpressionParser parser) if (typeParser.LineNumber >= 0) throw new TypeWasAlreadyParsed(this); //ncrunch: no coverage typeParser.ParseMembersAndMethods(parser); + DetermineEnumTypeKind(); ValidateMethodAndMemberCountLimits(); // ReSharper disable once ForCanBeConvertedToForeach, for performance reasons: // https://codeblog.jonskeet.uk/2009/01/29/for-vs-foreach-on-arrays-and-lists/ @@ -100,6 +163,17 @@ public Type ParseMembersAndMethods(ExpressionParser parser) return this; } + private void DetermineEnumTypeKind() + { + if (methods.Count == 0 && members.Count > 0) + { + for (var i = 0; i < members.Count; i++) + if (!members[i].IsConstant && !members[i].Type.IsEnum) + return; + typeKind = TypeKind.Enum; + } + } + public class TypeWasAlreadyParsed(Type type) : Exception(type.ToString()); //ncrunch: no coverage public sealed class MustImplementAllTraitMethodsOrNone(Type type, string traitName, @@ -113,15 +187,16 @@ private void ValidateMethodAndMemberCountLimits() : Limit.MemberCount; if (members.Count > memberLimit) throw new MemberCountShouldNotExceedLimit(this, memberLimit); - if (IsDataType || IsEnum) + if (IsEnum || IsDataType) return; - if (methods.Count == 0 && members.Count < 2 && !IsNoneAnyOrBoolean() && Name != Base.Name) + if (methods.Count == 0 && members.Count < 2 && typeKind == TypeKind.Unknown) throw new NoMethodsFound(this, typeParser.LineNumber); - if (methods.Count > Limit.MethodCount && (Package.Name != nameof(Base) && + if (methods.Count > Limit.MethodCount && (Package.Name != nameof(Strict) && Package.Name != "TestPackage" || Name == "MethodCountMustNotExceedFifteen")) throw new MethodCountMustNotExceedLimit(this); } + public bool IsEnum => typeKind == TypeKind.Enum; /// /// Data types have no methods and just some data. Number, Text, and most Base types are not /// data types as they have functionality (which makes sense), only types higher up that only @@ -129,8 +204,8 @@ private void ValidateMethodAndMemberCountLimits() /// public bool IsDataType => CheckIfParsed() && methods.Count == 0 && - (members.Count > 1 || members is [{ InitialValue: not null }]) || Name == Base.Number || - Name == Base.Name; + (members.Count > 1 || members is [{ InitialValue: not null }]) || Name == Number || + Name == nameof(Name); private bool CheckIfParsed() { @@ -142,16 +217,9 @@ private bool CheckIfParsed() private sealed class TypeIsNotParsedCallParseMembersAndMethods(Type type) : Exception(type.ToString()); //ncrunch: no coverage - private bool IsParsed => IsGeneric || Lines.Length <= 1 || typeParser.LineNumber != -1; - public bool IsEnum => - CheckIfParsed() && methods.Count == 0 && members.Count > 0 && - members.All(m => m.IsConstant || m.Type.IsEnum); - public sealed class MemberCountShouldNotExceedLimit(Type type, int limit) : ParsingFailed(type, 0, $"{type.Name} type has {type.members.Count} members, max: {limit}"); - private bool IsNoneAnyOrBoolean() => Name is Base.None or Base.Any or Base.Boolean or Base.Mutable; - public sealed class NoMethodsFound(Type type, int lineNumber) : ParsingFailed(type, lineNumber, "Each type must have at least two members (datatypes and enums) or at least one method, " + "otherwise it is useless"); @@ -175,8 +243,7 @@ private void CheckIfTraitIsImplementedFullyOrNone(Type trait) protected readonly List members = []; public List Methods => methods; protected readonly List methods = []; - public bool IsTrait => - Name != Base.Number && Name != Base.Boolean && CheckIfParsed() && Members.Count == 0; + public bool IsTrait => !IsNumber && !IsBoolean && CheckIfParsed() && Members.Count == 0; public Dictionary AvailableMemberTypes { get @@ -194,14 +261,15 @@ public Dictionary AvailableMemberTypes } public override Type? FindType(string name, Context? searchingFrom = null) => - name == Name || name.Contains('.') && name == base.ToString() || name is Other or Outer + name == Name || name is Other or Outer || name == FullName ? this : Package.FindType(name, searchingFrom ?? this); /// - /// Only internally used, cannot be specified as member, parameter or variable. Everything is Any. + /// Everything internally is Any, cannot be specified as member, parameter, or variable. /// public const string AnyLowercase = "any"; + public const string GenericUppercase = "Generic"; public const string GenericLowercase = "generic"; public const string IteratorLowercase = "iterator"; public const string ElementsLowercase = "elements"; @@ -223,11 +291,25 @@ public GenericTypeImplementation GetGenericImplementation(params Type[] implemen return GetGenericImplementation(key) ?? CreateGenericImplementation(key, implementationTypes); } - internal string GetImplementationName(IEnumerable implementationTypes) => - Name + "(" + implementationTypes.Select(t => t.Name).ToWordList() + ")"; + internal string GetImplementationName(IReadOnlyList implementationTypes) + { + var key = ""; + for (var i = 0; i < implementationTypes.Count; i++) + key += (key == "" + ? "" + : ", ") + implementationTypes[i].Name; + return Name + "(" + key + ")"; + } - internal string GetImplementationName(IEnumerable implementationTypes) => - Name + "(" + implementationTypes.Select(t => t.Name + " " + t.Type).ToWordList() + ")"; + internal string GetImplementationName(IReadOnlyList implementationTypes) + { + var key = ""; + for (var i = 0; i < implementationTypes.Count; i++) + key += (key == "" + ? "" + : ", ") + implementationTypes[i]; + return Name + "(" + key + ")"; + } private GenericTypeImplementation? GetGenericImplementation(string key) { @@ -245,7 +327,7 @@ internal string GetImplementationName(IEnumerable implementationTypes private GenericTypeImplementation CreateGenericImplementation(string key, IReadOnlyList implementationTypes) { - if (Name is Base.List or Base.Iterator or Base.Mutable && implementationTypes.Count == 1 || + if (Name is List or Iterator or Mutable && implementationTypes.Count == 1 || GetGenericTypeArguments().Count == implementationTypes.Count || HasMatchingConstructor(implementationTypes)) { @@ -262,7 +344,12 @@ private bool HasMatchingConstructor(IReadOnlyList implementationTypes) => public sealed class CannotGetGenericImplementationOnNonGeneric(string name, string key) : Exception("Type: " + name + ", Generic Implementation: " + key); - public string FilePath => Path.Combine(Package.FolderPath, Name) + Extension; + public string FilePath => + Path.GetFullPath(Path.Combine( + Package.FolderPath ?? Repositories.GetLocalDevelopmentPath(Repositories.StrictOrg, "Strict"), + (this is GenericTypeImplementation genericType + ? genericType.Generic.Name + : Name) + Extension)); public const string Extension = ".strict"; public Member? FindMember(string name) @@ -281,14 +368,13 @@ public class GenericTypesCannotBeUsedDirectlyUseImplementation(Type type, string extraInformation) : Exception(type + " " + extraInformation); /// - /// Any non-public member is automatically iterable if it has Iterator, for example, - /// Text.strict or Error.strict have public members you have to iterate over yourself. - /// If there are two private iterators, then pick the first member automatically. - /// Any number is also iteratable, most iterators are just List(ofSomeType) + /// Any non-public member is automatically iterable if it has Iterator, for example, Text.strict + /// or Error.strict have public members you have to iterate over yourself. If there are more + /// private iterators, pick the first member automatically. List and number are also iterable. /// public bool IsIterator => - Name == Base.Iterator || Name.StartsWith(Base.Iterator + "(", StringComparison.Ordinal) || - HasAnyIteratorMember(); + typeKind == TypeKind.Iterator || + Name.StartsWith(Iterator + "(", StringComparison.Ordinal) || HasAnyIteratorMember(); private bool HasAnyIteratorMember() { @@ -317,37 +403,65 @@ private bool ExecuteIsIteratorCheck() private readonly Dictionary cachedEvaluatedMemberTypes = new(); /// - /// Can OUR type be converted to sameOrUpcastableType and be used as such? Be careful how this is + /// Can OUR type be converted to sameOrUsableType and be used as such? Be careful how this is /// called. A derived RedApple can be used as the base class Apple, but not the other way around. /// public bool IsSameOrCanBeUsedAs(Type sameOrUsableType, bool allowImplicitConversion = true, int maxDepth = 2) { - if (this == sameOrUsableType || sameOrUsableType.Name == Base.Any) + if (this == sameOrUsableType || sameOrUsableType.IsAny) + return true; + if (allowImplicitConversion && IsImplicitToConversion(sameOrUsableType)) + return true; + if (IsEnum && members[0].Type.IsSameOrCanBeUsedAs(sameOrUsableType)) return true; - if (members.Count(m => m.Type == sameOrUsableType) == 1) + if (HasExactlyOneMemberOfType(sameOrUsableType)) return true; if (IsMutableAndHasMatchingInnerType(sameOrUsableType) || sameOrUsableType.IsMutableAndHasMatchingInnerType(this)) return true; - if (allowImplicitConversion && IsImplicitToConversion(sameOrUsableType)) - return true; if (IsCompatibleOneOfType(sameOrUsableType)) return true; - if (IsParsed && IsEnum && members[0].Type.IsSameOrCanBeUsedAs(sameOrUsableType)) - return true; - return maxDepth >= 0 && Members.Count(m => !m.IsConstant && - m.Type.IsSameOrCanBeUsedAs(sameOrUsableType, allowImplicitConversion, maxDepth - 1)) == 1; + return maxDepth >= 0 && + HasExactlyOneUsableMember(sameOrUsableType, allowImplicitConversion, maxDepth); + } + + private bool HasExactlyOneMemberOfType(Type targetType) + { + // Basically members.Count(m => m.Type == targetType) == 1, but more performant + var found = false; + foreach (var m in members) + if (m.Type == targetType) + { + if (found) + return false; + found = true; + } + return found; + } + + private bool HasExactlyOneUsableMember(Type targetType, bool allowImplicitConversion, int maxDepth) + { + var found = false; + foreach (var m in Members) + if (!m.IsConstant && + m.Type.IsSameOrCanBeUsedAs(targetType, allowImplicitConversion, maxDepth - 1)) + { + if (found) + return false; + found = true; + } + return found; } /// /// Only allow implicit conversions as defined in Any.strict (to Text, to Type, to HashCode) /// private static bool IsImplicitToConversion(Context targetType) => - targetType.Name is Base.Text or Base.Type or Base.HashCode; + targetType.Name is Text or nameof(Type) or HashCode; internal bool IsMutableAndHasMatchingInnerType(Type argumentType) => - this is GenericTypeImplementation { Generic.Name: Base.Mutable } genericTypeImplementation && + this is GenericTypeImplementation { Generic.Name: Mutable } genericTypeImplementation && genericTypeImplementation.ImplementationTypes[0].IsSameOrCanBeUsedAs(argumentType); private bool IsCompatibleOneOfType(Type sameOrBaseType) => @@ -364,9 +478,9 @@ private bool IsCompatibleOneOfType(Type sameOrBaseType) => if (IsError) return elseType; // Allow number and iterators for return types - if (Name == Base.Number && elseType.IsIterator) + if (Name == Number && elseType.IsIterator) return elseType; - if (elseType.Name == Base.Number && IsIterator) + if (elseType.IsNumber && IsIterator) return this; foreach (var member in members) if (elseType.members.Any(otherMember => otherMember.Type == member.Type)) @@ -402,7 +516,7 @@ public IReadOnlyDictionary> AvailableMethods foreach (var method in methods) if (method.IsPublic || method.Name == Method.From || method.Name.AsSpan().IsOperator()) AddAvailableMethod(method); - if (Name == Base.Any) + if (Name == Any) return cachedAvailableMethods; // Types are composed in Strict, we want users to be able to use base methods but exclude // public members (e.g., Type.Name), constants (e.g., constant Tab = Character(7)) and if we @@ -414,9 +528,8 @@ public IReadOnlyDictionary> AvailableMethods methods.All(m => m.Name != Method.From)) AddFromConstructorWithMembersAsArguments(methods.Count > 0 ? methods[0].Parser - : GetType(Base.Any).AvailableMethods.First().Value[0].Parser); - if (this is GenericTypeImplementation dictImpl && - dictImpl.Generic.Name == Base.Dictionary && + : GetType(Any).AvailableMethods.First().Value[0].Parser); + if (this is GenericTypeImplementation { Generic.IsDictionary: true } dictImpl && dictImpl.Generic.AvailableMethods.TryGetValue(Method.From, out var genericFromMethods) && cachedAvailableMethods!.TryGetValue(Method.From, out var existingFromMethods)) foreach (var fromMethod in genericFromMethods) @@ -469,7 +582,7 @@ private string CreateFromMethodParameters() member.Name.MakeFirstLetterLowercase() + (member.InitialValue != null ? " = " + member.InitialValue - : member.Type.Name == Base.List + : member.Type.Name == List ? "" : " " + member.Type.Name); return parameters; @@ -497,7 +610,7 @@ private void AddNonGenericMethods(Type implementType) private void AddAnyMethods() { - cachedAnyMethods ??= GetType(Base.Any).AvailableMethods; + cachedAnyMethods ??= GetType(Any).AvailableMethods; if (!IsGeneric) foreach (var (_, anyMethods) in cachedAnyMethods) foreach (var anyMethod in anyMethods) @@ -508,14 +621,14 @@ private void AddAnyMethods() public sealed class NoMatchingMethodFound(Type type, string methodName, IReadOnlyDictionary> availableMethods) : Exception("\"" + methodName + - "\" not found for " + type + ", available methods: " + availableMethods.Keys.ToWordList()); + "\" not found for " + type + ", available methods: " + string.Join(", ", availableMethods.Keys)); public sealed class ArgumentsDoNotMatchMethodParameters(IReadOnlyList arguments, Type type, IEnumerable allMethods) : Exception((arguments.Count == 0 ? "No arguments does " : (arguments.Count == 1 ? "Argument: " - : "Arguments: ") + arguments.Select(a => a.ToStringWithType()).ToWordList() + " do ") + + : "Arguments: ") + string.Join(", ", arguments.Select(a => a.ToStringWithType())) + " do ") + "not match these " + type + " method(s):\n" + string.Join("\n", allMethods)); public bool IsUpcastable(Type otherType) => @@ -536,8 +649,8 @@ public HashSet GetGenericTypeArguments() if (member.Type is GenericType genericType) foreach (var namedType in genericType.GenericImplementations) genericArguments.Add(namedType); - else if (member.Type.Name == Base.List || member.Type.IsIterator) - genericArguments.Add(new Parameter(this, Base.Generic)); + else if (member.Type.IsList || member.Type.IsIterator) + genericArguments.Add(new Parameter(this, GenericUppercase)); else if (member.Type.IsGeneric) genericArguments.Add(member); return genericArguments.Count == 0 @@ -564,16 +677,25 @@ internal Expression GetMemberExpression(ExpressionParser parser, string memberNa string remainingTextSpan, int typeLineNumber) => typeParser.GetMemberExpression(parser, memberName, remainingTextSpan, typeLineNumber); - public bool IsMutable => - Name == Base.Mutable || this is GenericTypeImplementation { Generic.Name: Base.Mutable }; + public bool IsNone => typeKind == TypeKind.None; /// /// Is this a boolean or if OneOfType, is one of the types a boolean? Used to check for tests /// - public virtual bool IsBoolean => Name == Base.Boolean; - public bool IsError => - Name is Base.Error || this is GenericTypeImplementation { Generic.Name: Base.ErrorWithValue }; - public bool IsDictionary => this is GenericTypeImplementation { Generic.Name: Base.Dictionary }; - public void Dispose() => ((Package)Parent).Remove(this); + public virtual bool IsBoolean => typeKind == TypeKind.Boolean; + public bool IsText => typeKind == TypeKind.Text; + public bool IsNumber => typeKind == TypeKind.Number; + public bool IsCharacter => typeKind == TypeKind.Character; + public bool IsError => typeKind == TypeKind.Error; + public bool IsList => typeKind == TypeKind.List; + public bool IsDictionary => typeKind == TypeKind.Dictionary; + public bool IsMutable => typeKind == TypeKind.Mutable; + public bool IsAny => typeKind == TypeKind.Any; + + public void Dispose() + { + GC.SuppressFinalize(this); + ((Package)Parent).Remove(this); + } public int FindLineNumber(string firstLineThatContains) { @@ -582,4 +704,6 @@ public int FindLineNumber(string firstLineThatContains) return lineNumber; return -1; } + + public Type GetFirstImplementation() => ((GenericTypeImplementation)this).ImplementationTypes[0]; } \ No newline at end of file diff --git a/Strict.Language/TypeKind.cs b/Strict.Language/TypeKind.cs new file mode 100644 index 00000000..5a512bc8 --- /dev/null +++ b/Strict.Language/TypeKind.cs @@ -0,0 +1,21 @@ +namespace Strict.Language; + +/// +/// It is faster to check for common types via this value than comparing Types or Type.Names. +/// +public enum TypeKind : ushort +{ + None, + Boolean, + Number, + Text, + Character, + List, + Dictionary, + Error, + Enum, + Iterator, + Mutable, + Any, + Unknown +} \ No newline at end of file diff --git a/Strict.Language/TypeLines.cs b/Strict.Language/TypeLines.cs index 0e45ec43..5ac4e9ec 100644 --- a/Strict.Language/TypeLines.cs +++ b/Strict.Language/TypeLines.cs @@ -1,4 +1,4 @@ -namespace Strict.Language; +namespace Strict.Language; /// /// Optimization to split type parsing into three steps: @@ -16,11 +16,11 @@ public TypeLines(string name, params string[] lines) DependentTypes = ExtractDependentTypes(); #if DEBUG // Some sanity checks to make sure the Any base type used everywhere isn't broken - if (Name != Base.Any) + if (Name != Type.Any) return; AnyMustImplement(0, Method.From); - AnyMustImplement(1, "to " + Base.Type); - AnyMustImplement(2, "to " + Base.Text); + AnyMustImplement(1, "to " + nameof(Type)); + AnyMustImplement(2, "to " + Type.Text); #endif } #if DEBUG @@ -71,7 +71,7 @@ private void AddDependentType(string remainingLine, ref IList dependentT AddIfNotExisting(dependentTypes, part); else if (remainingLine.EndsWith('s')) { - AddIfNotExisting(dependentTypes, Base.List); + AddIfNotExisting(dependentTypes, Type.List); AddIfNotExisting(dependentTypes, remainingLine[..^1].MakeFirstLetterUppercase()); } else @@ -83,10 +83,11 @@ private void AddIfNotExisting(ICollection dependentTypes, string typeNam if (typeName.Contains(Keyword.With)) typeName = typeName[..typeName.IndexOf("with", StringComparison.Ordinal)].Trim(); else if (typeName.Contains(' ')) - typeName = typeName.Split(' ')[1]; - if (!dependentTypes.Contains(typeName.MakeFirstLetterUppercase()) && Name != typeName && - !typeName.IsKeyword() && typeName != Base.Generic) - dependentTypes.Add(typeName.MakeFirstLetterUppercase()); + typeName = typeName[(typeName.IndexOf(' ') + 1)..]; + var upperTypeName = typeName.MakeFirstLetterUppercase(); + if (!dependentTypes.Contains(upperTypeName) && Name != typeName && + !typeName.IsKeyword() && typeName != Type.GenericUppercase) + dependentTypes.Add(upperTypeName); } public override string ToString() => Name + DependentTypes.ToBrackets(); diff --git a/Strict.Language/TypeMethodFinder.cs b/Strict.Language/TypeMethodFinder.cs index e618e042..cd1d88c6 100644 --- a/Strict.Language/TypeMethodFinder.cs +++ b/Strict.Language/TypeMethodFinder.cs @@ -8,10 +8,18 @@ internal class TypeMethodFinder(Type type) public Type Type { get; } = type; public Method? FindFromMethodImplementation(IReadOnlyList implementationTypes) => - !Type.AvailableMethods.TryGetValue(Method.From, out var methods) - ? null - : methods.FirstOrDefault(m => IsMethodWithMatchingParametersType(m, implementationTypes, - TryGetSingleElementType(implementationTypes), Type)); + Type.AvailableMethods.TryGetValue(Method.From, out var methods) + ? FindFromMethod(implementationTypes, methods) + : null; + + private Method? FindFromMethod(IReadOnlyList implementationTypes, List methods) + { + for (var index = 0; index < methods.Count; index++) + if (IsMethodWithMatchingParametersType(methods[index], implementationTypes, + TryGetSingleElementType(implementationTypes), Type)) + return methods[index]; + return null; //ncrunch: no coverage + } public Method GetMethod(string methodName, IReadOnlyList arguments) => FindMethod(methodName, arguments) ?? @@ -20,8 +28,8 @@ public Method GetMethod(string methodName, IReadOnlyList arguments) public Method? FindMethod(string methodName, IReadOnlyList arguments) { if (Type.IsGeneric) - throw new GenericTypesCannotBeUsedDirectlyUseImplementation(Type, Type.Name == Base.Mutable - ? Base.Mutable + " must be used via keyword, not manually constructed!" + throw new GenericTypesCannotBeUsedDirectlyUseImplementation(Type, Type.IsMutable + ? Mutable + " must be used via keyword, not manually constructed!" : "Type is Generic and cannot be used directly"); return Type is OneOfType ? FindMethodWithOneOfType(methodName, arguments) @@ -43,7 +51,7 @@ public Method GetMethod(string methodName, IReadOnlyList arguments) { if (!Type.AvailableMethods.TryGetValue(methodName, out var matchingMethods)) return null; - if (arguments.Count == 2 && arguments[0].ReturnType.IsError) + if (arguments is [{ ReturnType.IsError: true }, _]) return matchingMethods[0]; var typesOfArguments = arguments.Select(argument => argument.ReturnType).ToList(); var commonTypeOfArguments = TryGetSingleElementType(typesOfArguments); @@ -55,9 +63,10 @@ public Method GetMethod(string methodName, IReadOnlyList arguments) // Single character text can always be used as a character (thus number) if (arguments.Count == 1 && matchingMethods.Count > 0 && matchingMethods[0].Parameters.Count > 0 && - matchingMethods[0].Parameters[0].Type.Name is Base.Number or Base.Character && - arguments[0].ReturnType.Name == Base.Text && arguments[0].IsConstant && - arguments[0].GetType().Name == "Text" && GetTextValue(arguments[0]).Length == 1) + (matchingMethods[0].Parameters[0].Type.IsNumber || + matchingMethods[0].Parameters[0].Type.IsCharacter) && + arguments[0].ReturnType.IsText && arguments[0].IsConstant && + arguments[0].GetType().Name == Text && GetTextValue(arguments[0]).Length == 1) return matchingMethods[0]; // If this is a from constructor, we can call the methodParameterType constructor to pass // along the argument and make it work if it wasn't matching yet. @@ -72,17 +81,28 @@ matchingMethods[0].Parameters[0].Type.Name is Base.Number or Base.Character && throw new ArgumentsDoNotMatchMethodParameters(arguments, Type, matchingMethods); } - private static string GetTextValue(Expression argument) => - argument.GetType().GetProperty("Data", BindingFlags.Instance | BindingFlags.Public)?. - GetValue(argument)?.ToString() ?? ""; + private static string GetTextValue(Expression argument) + { + var data = argument.GetType(). + GetProperty("Data", BindingFlags.Instance | BindingFlags.Public)?.GetValue(argument); + if (data is string value) + return value; //ncrunch: no coverage + var text = data?.ToString() ?? argument.ToString(); + const string ValueInstanceTextPrefix = "Text: \""; + if (text.StartsWith(ValueInstanceTextPrefix, StringComparison.Ordinal) && text.EndsWith("\"", StringComparison.Ordinal)) + return text[ValueInstanceTextPrefix.Length..^1]; + return text.Length >= 2 && text[0] == '"' && text[^1] == '"' //ncrunch: no coverage + ? text[1..^1] + : text; + } - private static T? TryGetSingleElementType(IEnumerable argumentTypes) where T : class + private static T? TryGetSingleElementType(IReadOnlyList argumentTypes) where T : class { T? firstType = null; - foreach (var type in argumentTypes) + for (var i = 0; i < argumentTypes.Count; i++) if (firstType == null) - firstType = type; - else if (firstType != type) + firstType = argumentTypes[i]; + else if (firstType != argumentTypes[i]) return null; return firstType; } @@ -94,7 +114,7 @@ method.Parameters is { Type: GenericTypeImplementation { - Generic.Name: Base.List + Generic.Name: List } parameterType } ] && IsFromConstructorWithMatchingConstraints(method, numberOfArguments) @@ -105,25 +125,74 @@ private static bool IsFromConstructorWithMatchingConstraints(Method method, int { if (method.Name != Method.From) return true; - var member = method.Type.Members.FirstOrDefault(m => !m.IsConstant && m.Type.Name != Base.Iterator); - return member?.Constraints == null || - member.Constraints[0].ToString().Contains("Length is " + numberOfArguments); //TODO: do actual evaluation of constraint + var member = method.Type.Members.FirstOrDefault(m => !m.IsConstant && m.Type.Name != Iterator); + if (member?.Constraints == null) + return true; + return ConstraintCouldBeSatisfiedByArgumentCount(member.Constraints[0], numberOfArguments); } + private static bool ConstraintCouldBeSatisfiedByArgumentCount(Expression constraint, int numberOfArguments) + { + var constraintText = constraint.ToString(); + // Check for "Length is N" pattern + if (constraintText.Contains("Length is ")) + return constraintText.Contains("Length is " + numberOfArguments); + // Check for "Length > N", "Length >= N", "Length < N", "Length <= N" patterns + //ncrunch: no coverage start + if (constraintText.Contains("Length")) + { + if (constraintText.Contains("> 0")) + return numberOfArguments > 0; + if (constraintText.Contains(">= ")) + return TryExtractNumberAndCompare(constraintText, ">=", numberOfArguments); + if (constraintText.Contains("> ")) + return TryExtractNumberAndCompare(constraintText, ">", numberOfArguments); + if (constraintText.Contains("< ")) + return TryExtractNumberAndCompare(constraintText, "<", numberOfArguments); + if (constraintText.Contains("<= ")) + return TryExtractNumberAndCompare(constraintText, "<=", numberOfArguments); + } + // For other constraint types (like " " is not in value), we assume they could pass + return true; + } + + private static bool TryExtractNumberAndCompare(string constraintText, string op, int numberOfArguments) + { + var opIndex = constraintText.IndexOf(op, StringComparison.Ordinal); + if (opIndex < 0) + return true; + var afterOp = constraintText[(opIndex + op.Length)..].TrimStart(); + var numberEndIndex = 0; + while (numberEndIndex < afterOp.Length && char.IsDigit(afterOp[numberEndIndex])) + numberEndIndex++; + if (numberEndIndex == 0) + return true; + if (!int.TryParse(afterOp[..numberEndIndex], out var constraintNumber)) + return true; + return op switch + { + ">" => numberOfArguments > constraintNumber, + ">=" => numberOfArguments >= constraintNumber, + "<" => numberOfArguments < constraintNumber, + "<=" => numberOfArguments <= constraintNumber, + _ => true + }; + } //ncrunch: no coverage end + private static bool IsMethodWithMatchingParametersType(Method method, IReadOnlyList typesOfArguments, Type? commonTypeOfArguments, Type currentType) { // Allow `is`/`is not` comparisons against our own type (Range is Range), those are mostly not // implemented. Also, always allow comparison against Errors for error checking. if (method.Name is BinaryOperator.Is or UnaryOperator.Not && method.Parameters.Count > 0 && - method.Parameters[0].Type.Name == Base.Any && (commonTypeOfArguments == currentType || - commonTypeOfArguments?.Name == Base.Error)) - return true; + method.Parameters[0].Type.IsAny && (commonTypeOfArguments == currentType || + (commonTypeOfArguments?.IsError ?? false))) + return true; //ncrunch: no coverage if (method is { Name: Method.From, Parameters.Count: 0 } && typesOfArguments.Count == 1 && method.ReturnType.IsSameOrCanBeUsedAs(typesOfArguments[0], false)) return true; //ncrunch: no coverage if (typesOfArguments.Count > method.Parameters.Count || typesOfArguments.Count < - method.Parameters.Count(p => p.DefaultValue == null)) + GetMethodParameterDefaultValueCount(method)) return false; for (var index = 0; index < typesOfArguments.Count; index++) if (!IsMethodParameterMatchingArgument(method, index, typesOfArguments[index])) @@ -131,18 +200,27 @@ private static bool IsMethodWithMatchingParametersType(Method method, return true; } + private static int GetMethodParameterDefaultValueCount(Method method) + { + var count = 0; + for (var index = 0; index < method.Parameters.Count; index++) + if (method.Parameters[index].DefaultValue == null) + count++; + return count; + } + private static bool IsMethodParameterMatchingArgument(Method method, int index, Type argumentType) { var methodParameterType = method.Parameters[index].Type; - if (methodParameterType is GenericTypeImplementation { Generic.Name: Base.Mutable } mutableType) - methodParameterType = mutableType.ImplementationTypes[0]; + if (methodParameterType.IsMutable) + methodParameterType = methodParameterType.GetFirstImplementation(); if (argumentType == methodParameterType || method.IsGeneric || IsArgumentImplementationTypeMatchParameterType(argumentType, methodParameterType)) return true; - if (methodParameterType.Name != Base.Text && methodParameterType.IsEnum && + if (methodParameterType is { IsText: false, IsEnum: true } && methodParameterType.Members[0].Type.IsSameOrCanBeUsedAs(argumentType)) return true; - if (methodParameterType.Name == Base.Iterator && method.Type.IsSameOrCanBeUsedAs(argumentType)) + if (methodParameterType.Name == Type.Iterator && method.Type.IsSameOrCanBeUsedAs(argumentType)) return true; //ncrunch: no coverage if (!methodParameterType.IsGeneric) return argumentType.IsSameOrCanBeUsedAs(methodParameterType); diff --git a/Strict.Language/TypeParser.cs b/Strict.Language/TypeParser.cs index 300236a2..e022df7b 100644 --- a/Strict.Language/TypeParser.cs +++ b/Strict.Language/TypeParser.cs @@ -166,7 +166,7 @@ private static HashSet CollectParameterNamesFromSignature(string signatu /// private void DetectSelfRecursionWithSameArguments(IReadOnlyList methodLines) { - if (methodLines.Count == 0 || type.Name == Base.System) + if (methodLines.Count == 0 || type.Name == Type.System) return; var signature = methodLines[0]; var openParen = signature.IndexOf('('); @@ -390,7 +390,7 @@ private Member ParseMember(ExpressionParser parser, ReadOnlySpan remaining } catch (Exception ex) { - throw new ParsingFailed(type, LineNumber, ex.Message.Split('\n').Take(2).ToWordList("\n"), ex); + throw new ParsingFailed(type, LineNumber, ex.Message.Split('\n').Take(2).ToLines(), ex); } } @@ -429,7 +429,7 @@ private Member TryParseMember(ExpressionParser parser, ReadOnlySpan remain return IsMemberTypeAny(nameAndType, nameAndExpression) ? throw new MemberWithTypeAnyIsNotAllowed(type, LineNumber, nameAndType) : usedKeyword == Keyword.Constant - ? new Member(type, nameAndType, type.GetType(Base.Number), LineNumber, usedKeyword) + ? new Member(type, nameAndType, type.GetType(Type.Number), LineNumber, usedKeyword) { InitialValue = GetMemberExpression(parser, nameAndType, (type.AutogeneratedEnumValue++).ToString(), LineNumber) @@ -456,11 +456,11 @@ private Type GetInitialValueType(ExpressionParser parser, string nameAndType, if (memberNameType != null && !constantValue.StartsWith(memberNameWithFirstLetterCaps)) return memberNameType; if (constantValue.StartsWith('\"')) - return type.GetType(Base.Text); + return type.GetType(Type.Text); if (constantValue is "true" || constantValue is "false") - return type.GetType(Base.Boolean); //ncrunch: no coverage + return type.GetType(Type.Boolean); //ncrunch: no coverage return constantValue.TryParseNumber(out _) - ? type.GetType(Base.Number) + ? type.GetType(Type.Number) : GetMemberExpression(parser, nameAndType, constantValue, LineNumber).ReturnType; } @@ -545,7 +545,7 @@ public sealed class MemberMissingConstraintExpression(Type type, int lineNumber, private static bool IsMemberTypeAny(string nameAndType, SpanSplitEnumerator nameAndExpression) => nameAndType == Type.AnyLowercase || - nameAndExpression.Current.Equals(Base.Any, StringComparison.Ordinal); + nameAndExpression.Current.Equals(Type.Any, StringComparison.Ordinal); public sealed class MemberWithTypeAnyIsNotAllowed(Type type, int lineNumber, string name) : ParsingFailed(type, lineNumber, name); diff --git a/Strict.Language/UnaryOperator.cs b/Strict.Language/UnaryOperator.cs index 9350c68e..2eb025ae 100644 --- a/Strict.Language/UnaryOperator.cs +++ b/Strict.Language/UnaryOperator.cs @@ -2,6 +2,5 @@ public static class UnaryOperator { - public const string Minus = "-"; public const string Not = "not"; } \ No newline at end of file diff --git a/Strict.LanguageServer.Tests/AutoCompletorTests.cs b/Strict.LanguageServer.Tests/AutoCompletorTests.cs index f8b51ebf..2d52c979 100644 --- a/Strict.LanguageServer.Tests/AutoCompletorTests.cs +++ b/Strict.LanguageServer.Tests/AutoCompletorTests.cs @@ -1,8 +1,10 @@ using NUnit.Framework; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using Strict.Language; using Strict.Expressions; +using Strict.Language; +using Strict.Language.Tests; +using Type = Strict.Language.Type; namespace Strict.LanguageServer.Tests; @@ -15,7 +17,7 @@ public async Task LoadStrictPackageAsync() => private Package package = null!; [SetUp] - public void CreateStrictDocument() => strictDocument = new StrictDocument(); + public void CreateStrictDocument() => strictDocument = new StrictDocument(TestPackage.Instance); private StrictDocument strictDocument = null!; @@ -71,7 +73,7 @@ public async Task HandleLogAutoCompleteAsync(string completionName, int lineNumb } private static DocumentUri GetDocumentUri(string seed) => - new("", "", $"Test{seed}.strict", "", ""); + new("", "", "Test" + seed + Type.Extension, "", ""); [TestCase(2, "Write", "has logger", "Log(message Text)", "\trandom.")] [TestCase(1, "Write", "has logger", "has some Text", "Log(message Text)", "\trandom.")] diff --git a/Strict.LanguageServer.Tests/LanguageServerTests.cs b/Strict.LanguageServer.Tests/LanguageServerTests.cs index ec50bce3..180db446 100644 --- a/Strict.LanguageServer.Tests/LanguageServerTests.cs +++ b/Strict.LanguageServer.Tests/LanguageServerTests.cs @@ -1,4 +1,4 @@ -using Moq; +using Moq; using NUnit.Framework; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Server; @@ -8,7 +8,8 @@ namespace Strict.LanguageServer.Tests; public class LanguageServerTests { - protected static readonly DocumentUri URI = new("", "", "Test/Test.strict", "", ""); + protected static readonly DocumentUri URI = new("", "", "Test/Test" + Language.Type.Extension, + "", ""); protected TextDocumentSynchronizer textDocumentHandler = null!; protected Mock languageServer = null!; @@ -22,7 +23,7 @@ public void MockHandlers() languageServer.Setup(expression => expression.TextDocument). Returns(new Mock().Object); textDocumentHandler = - new TextDocumentSynchronizer(languageServer.Object, new StrictDocument(), TestPackage.Instance); + new TextDocumentSynchronizer(languageServer.Object, new StrictDocument(TestPackage.Instance), TestPackage.Instance); textDocumentHandler.Document.AddOrUpdate(URI, "constant bla = 5"); } } \ No newline at end of file diff --git a/Strict.LanguageServer.Tests/Strict.LanguageServer.Tests.csproj b/Strict.LanguageServer.Tests/Strict.LanguageServer.Tests.csproj index df5843db..dd9495df 100644 --- a/Strict.LanguageServer.Tests/Strict.LanguageServer.Tests.csproj +++ b/Strict.LanguageServer.Tests/Strict.LanguageServer.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/Strict.LanguageServer.Tests/TextDocumentSynchronizerTests.cs b/Strict.LanguageServer.Tests/TextDocumentSynchronizerTests.cs index d0fd9610..ff4e8123 100644 --- a/Strict.LanguageServer.Tests/TextDocumentSynchronizerTests.cs +++ b/Strict.LanguageServer.Tests/TextDocumentSynchronizerTests.cs @@ -16,7 +16,8 @@ public void MultiLineSetup() textDocumentHandler.Document.InitializeContent(MultiLineURI); } - private static readonly DocumentUri MultiLineURI = new("", "", "Test/MultiLine.strict", "", ""); + private static readonly DocumentUri MultiLineURI = + new("", "", "Test/MultiLine" + Language.Type.Extension, "", ""); private static IEnumerable TextDocumentChangeCases { //ncrunch: no coverage start diff --git a/Strict.LanguageServer/BaseSelectors.cs b/Strict.LanguageServer/BaseSelectors.cs index b7b74e76..e76c2747 100644 --- a/Strict.LanguageServer/BaseSelectors.cs +++ b/Strict.LanguageServer/BaseSelectors.cs @@ -6,6 +6,5 @@ namespace Strict.LanguageServer; public static class BaseSelectors { public static readonly TextDocumentSelector StrictDocumentSelector = - new( - new TextDocumentFilter { Pattern = "**/*.strict" }); -} //ncrunch: no coverage end \ No newline at end of file + new(new TextDocumentFilter { Pattern = "**/*" + Language.Type.Extension }); +} \ No newline at end of file diff --git a/Strict.LanguageServer/CommandExecutor.cs b/Strict.LanguageServer/CommandExecutor.cs index 88780dbc..44172316 100644 --- a/Strict.LanguageServer/CommandExecutor.cs +++ b/Strict.LanguageServer/CommandExecutor.cs @@ -15,7 +15,7 @@ namespace Strict.LanguageServer; public class CommandExecutor(ILanguageServerFacade languageServer, StrictDocument document, Package package) : IExecuteCommandHandler { - private readonly BytecodeInterpreter vm = new(); + private readonly BytecodeInterpreter vm = new(package); private const string CommandName = "strict-vscode-client.run"; Task IRequestHandler.Handle( diff --git a/Strict.LanguageServer/RunnerService.cs b/Strict.LanguageServer/RunnerService.cs index d5bbf02a..c12341ee 100644 --- a/Strict.LanguageServer/RunnerService.cs +++ b/Strict.LanguageServer/RunnerService.cs @@ -1,10 +1,11 @@ -using Strict.Runtime; +using Strict.Language; +using Strict.Runtime; namespace Strict.LanguageServer; -public class RunnerService +public class RunnerService(Package package) { - private BytecodeInterpreter VmInstance { get; } = new(); + private BytecodeInterpreter VmInstance { get; } = new(package); private readonly List services = new(); public RunnerService AddService(RunnableService runnableService) diff --git a/Strict.LanguageServer/Strict.LanguageServer.csproj b/Strict.LanguageServer/Strict.LanguageServer.csproj index 3d32efcf..3c2bc01d 100644 --- a/Strict.LanguageServer/Strict.LanguageServer.csproj +++ b/Strict.LanguageServer/Strict.LanguageServer.csproj @@ -17,7 +17,7 @@ - + diff --git a/Strict.LanguageServer/StrictDocument.cs b/Strict.LanguageServer/StrictDocument.cs index bc18f65f..1e333b6d 100644 --- a/Strict.LanguageServer/StrictDocument.cs +++ b/Strict.LanguageServer/StrictDocument.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Immutable; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -10,11 +10,11 @@ namespace Strict.LanguageServer; -public sealed class StrictDocument +public sealed class StrictDocument(Package package) { private readonly ConcurrentDictionary strictDocuments = new(); private List content = []; - private readonly BytecodeInterpreter vm = new(); + private readonly BytecodeInterpreter vm = new(package); public void Update(DocumentUri uri, TextDocumentContentChangeEvent[] changes) { @@ -134,9 +134,9 @@ private void ParseCurrentFile(Package package, DocumentUri uri, ILanguageServerF var methods = ParseTypeMethods(type.Methods); if (methods != null) // @formatter:off - new RunnerService() - .AddService(new TestRunner(languageServer,methods)) - .AddService(new VariableValueEvaluator(languageServer, Get(uri))) + new RunnerService(package) + .AddService(new TestRunner(package, languageServer,methods)) + .AddService(new VariableValueEvaluator(package, languageServer, Get(uri))) .RunAllServices(); // @formatter:on } diff --git a/Strict.LanguageServer/TestRunner.cs b/Strict.LanguageServer/TestRunner.cs index cad25013..81c2c97f 100644 --- a/Strict.LanguageServer/TestRunner.cs +++ b/Strict.LanguageServer/TestRunner.cs @@ -1,4 +1,4 @@ -using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; using Strict.Language; using Strict.Expressions; using Strict.Runtime; @@ -6,8 +6,8 @@ namespace Strict.LanguageServer; //ncrunch: no coverage start -public sealed class TestRunner(ILanguageServerFacade languageServer, IEnumerable methods) - : RunnerService, RunnableService +public sealed class TestRunner(Package package, ILanguageServerFacade languageServer, IEnumerable methods) + : RunnerService(package), RunnableService { private IEnumerable Methods { get; } = methods; private const string NotificationName = "testRunnerNotification"; @@ -20,7 +20,7 @@ public void Run(BytecodeInterpreter vm) var output = vm. Execute(new ByteCodeGenerator((MethodCall)methodCall.Instance).Generate()).Returns; languageServer?.SendNotification(NotificationName, new TestNotificationMessage( - GetLineNumber(test), Equals(output?.Value, ((Value)methodCall.Arguments[0]).Data) + GetLineNumber(test), Equals(output, ((Value)methodCall.Arguments[0]).Data) ? TestState.Green : TestState.Red)); } diff --git a/Strict.LanguageServer/VariableStateNotificationMessage.cs b/Strict.LanguageServer/VariableStateNotificationMessage.cs index a2b84979..fc0eebb6 100644 --- a/Strict.LanguageServer/VariableStateNotificationMessage.cs +++ b/Strict.LanguageServer/VariableStateNotificationMessage.cs @@ -1,7 +1,7 @@ -namespace Strict.LanguageServer; +namespace Strict.LanguageServer; public sealed class VariableStateNotificationMessage(Dictionary lineTextPair) { - //ncrunch: no coverage start, TODO: missing tests + //ncrunch: no coverage start public Dictionary LineTextPair { get; } = lineTextPair; } \ No newline at end of file diff --git a/Strict.LanguageServer/VariableValueEvaluator.cs b/Strict.LanguageServer/VariableValueEvaluator.cs index 24c15e17..18d14930 100644 --- a/Strict.LanguageServer/VariableValueEvaluator.cs +++ b/Strict.LanguageServer/VariableValueEvaluator.cs @@ -1,10 +1,11 @@ -using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using Strict.Language; using Strict.Runtime; namespace Strict.LanguageServer; -public sealed class VariableValueEvaluator(ILanguageServerFacade languageServer, string[] lines) - : RunnerService, RunnableService +public sealed class VariableValueEvaluator(Package package, ILanguageServerFacade languageServer, string[] lines) + : RunnerService(package), RunnableService { private const string NotificationName = "valueEvaluationNotification"; @@ -13,10 +14,11 @@ public void Run(BytecodeInterpreter vm) var lineValuePair = new Dictionary(); for (var i = 0; i < lines.Length; i++) foreach (var variable in vm.Memory.Variables.Where(variable => - //ncrunch: no coverage start, TODO: missing tests + //ncrunch: no coverage start lines[i].Contains(variable.Key))) - lineValuePair[i] = - variable.Value.Value.ToString() ?? throw new InvalidOperationException(); + lineValuePair[i] = variable.Value.IsText + ? variable.Value.Text + : variable.Value.Number.ToString(); //ncrunch: no coverage end languageServer.SendNotification(NotificationName, new VariableStateNotificationMessage(lineValuePair)); diff --git a/Strict.Optimizers.Tests/Strict.Optimizers.Tests.csproj b/Strict.Optimizers.Tests/Strict.Optimizers.Tests.csproj index 0d1b5d83..175c3cc8 100644 --- a/Strict.Optimizers.Tests/Strict.Optimizers.Tests.csproj +++ b/Strict.Optimizers.Tests/Strict.Optimizers.Tests.csproj @@ -9,10 +9,8 @@ - - diff --git a/Strict.Optimizers/Class1.cs b/Strict.Optimizers/Class1.cs index 04966365..5dec6151 100644 --- a/Strict.Optimizers/Class1.cs +++ b/Strict.Optimizers/Class1.cs @@ -1,3 +1,3 @@ -namespace Strict.Optimizers; +namespace Strict.Optimizers; -public class Class1; \ No newline at end of file +public class Class1; //still need to work on this, this should be Runtime level optimizations, up to this point we just had Validators, constant collapsing, tests execution, etc. at runtime we want only the code that is needed for execution, so it will be much less, but still we want to optimize it! \ No newline at end of file diff --git a/Strict.PackageManager/Services/PackageManager.cs b/Strict.PackageManager/Services/PackageManager.cs index 8512640a..62ee030f 100644 --- a/Strict.PackageManager/Services/PackageManager.cs +++ b/Strict.PackageManager/Services/PackageManager.cs @@ -1,5 +1,5 @@ using Grpc.Core; -using System.IO.Compression; +using Strict.Language; namespace Strict.PackageManager.Services; @@ -7,19 +7,22 @@ namespace Strict.PackageManager.Services; public sealed class PackageManager(ILogger logger) : Strict.PackageManager.PackageManager.PackageManagerBase { - public override Task DownloadPackage(PackageDownloadRequest requestModel, - ServerCallContext context) => - (Task)DownloadAndExtract(requestModel.PackageUrl, - requestModel.PackageName, requestModel.TargetPath); - - private async Task DownloadAndExtract(string packageUrl, string packageName, - string targetPath) + public override async Task DownloadPackage(PackageDownloadRequest request, + ServerCallContext context) { - logger.LogTrace("Service invoked " + packageUrl + " " + packageName + " " + targetPath); - var localZip = Path.Combine(CacheFolder, packageName + ".zip"); - using HttpClient client = new(); - await DownloadFile(client, new Uri(packageUrl + "/archive/master.zip"), localZip); - await Task.Run(() => UnzipInCacheFolderAndMoveToTargetPath(packageName, targetPath, localZip)); + var parts = request.PackageUrl.Split('/'); + var org = parts[3]; + var localCachePath = Path.Combine(CacheFolder, org, request.PackageName); + logger.LogTrace("Service invoked " + request.PackageUrl + " " + request.PackageName + " " + + request.TargetPath); + if (!Directory.Exists(localCachePath)) + Directory.CreateDirectory(localCachePath); + using var downloader = new GitHubStrictDownloader(org, request.PackageName); + await downloader.DownloadFiles(localCachePath, context.CancellationToken); + if (!string.IsNullOrWhiteSpace(request.TargetPath) && + !request.TargetPath.Equals(localCachePath, StringComparison.OrdinalIgnoreCase)) + CopyToTargetPath(localCachePath, request.TargetPath); + return new EmptyReply(); } private static string CacheFolder => @@ -27,39 +30,11 @@ private async Task DownloadAndExtract(string packageUrl, string packageName, StrictPackages); private const string StrictPackages = nameof(StrictPackages); - private static async Task DownloadFile(HttpClient client, Uri uri, string fileName) - { - await using var stream = await client.GetStreamAsync(uri); - await using var file = new FileStream(fileName, FileMode.CreateNew); - await stream.CopyToAsync(file); - } - - private static void UnzipInCacheFolderAndMoveToTargetPath(string packageName, string targetPath, - string localZip) - { - ZipFile.ExtractToDirectory(localZip, CacheFolder, true); - var masterDirectory = Path.Combine(CacheFolder, packageName + "-master"); - if (!Directory.Exists(masterDirectory)) - throw new NoMasterFolderFoundFromPackage(packageName, localZip); - if (Directory.Exists(targetPath)) - new DirectoryInfo(targetPath).Delete(true); - TryMoveOrCopyWhenDeletionDidNotFullyWork(targetPath, masterDirectory); - } - - public sealed class NoMasterFolderFoundFromPackage(string packageName, string localZip) - : Exception(packageName + ", localZip: " + localZip); - - private static void TryMoveOrCopyWhenDeletionDidNotFullyWork(string targetPath, - string masterDirectory) + private static void CopyToTargetPath(string sourcePath, string targetPath) { - try - { - Directory.Move(masterDirectory, targetPath); - } - catch - { - foreach (var file in Directory.GetFiles(masterDirectory)) - File.Copy(file, Path.Combine(targetPath, Path.GetFileName(file)), true); - } + if (!Directory.Exists(targetPath)) + Directory.CreateDirectory(targetPath); + foreach (var file in Directory.GetFiles(sourcePath)) + File.Copy(file, Path.Combine(targetPath, Path.GetFileName(file)), true); } } \ No newline at end of file diff --git a/Strict.PackageManager/Strict.PackageManager.csproj b/Strict.PackageManager/Strict.PackageManager.csproj index a60cc7ab..a15581fd 100644 --- a/Strict.PackageManager/Strict.PackageManager.csproj +++ b/Strict.PackageManager/Strict.PackageManager.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/Strict.VirtualMachine.Tests/AdderProgramTests.cs b/Strict.Runtime.Tests/AdderProgramTests.cs similarity index 54% rename from Strict.VirtualMachine.Tests/AdderProgramTests.cs rename to Strict.Runtime.Tests/AdderProgramTests.cs index f40e2167..5b94a0b4 100644 --- a/Strict.VirtualMachine.Tests/AdderProgramTests.cs +++ b/Strict.Runtime.Tests/AdderProgramTests.cs @@ -1,9 +1,15 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Running; + namespace Strict.Runtime.Tests; -public sealed class AdderProgramTests : BaseVirtualMachineTests +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Throughput, warmupCount: 1, iterationCount: 10)] +public class AdderProgramTests : BaseVirtualMachineTests { [SetUp] - public void Setup() => vm = new BytecodeInterpreter(); + public void Setup() => vm = new BytecodeInterpreter(TestPackage.Instance); private BytecodeInterpreter vm = null!; private static readonly string[] AdderProgramCode = @@ -18,11 +24,13 @@ public sealed class AdderProgramTests : BaseVirtualMachineTests "\tresults" ]; - private List ExecuteAddTotals(string methodCall) => - ((IEnumerable)vm.Execute( + private List ExecuteAddTotals(string methodCall) + { + var result = vm.Execute( new ByteCodeGenerator(GenerateMethodCallFromSource("AdderProgram", - methodCall, AdderProgramCode)).Generate()).Returns!.Value). - Select(e => Convert.ToDecimal(((Value)e).Data)).ToList(); + methodCall, AdderProgramCode)).Generate()).Returns!.Value; + return result.List.Items.Select(item => (decimal)item.Number).ToList(); + } [Test] public void AddTotalsForSingleNumber() => @@ -32,8 +40,15 @@ public void AddTotalsForSingleNumber() => public void AddTotalsForTwoNumbers() => Assert.That(ExecuteAddTotals("AdderProgram(1, 2).AddTotals"), Is.EqualTo(new[] { 1m, 3m })); + //ncrunch: no coverage start [Test] + [Category("Slow")] + [Benchmark] public void AddTotalsForThreeNumbers() => Assert.That(ExecuteAddTotals("AdderProgram(1, 2, 3).AddTotals"), Is.EqualTo(new[] { 1m, 3m, 6m })); + + [Test] + [Category("Manual")] + public void BenchmarkCompare() => BenchmarkRunner.Run(); } diff --git a/Strict.VirtualMachine.Tests/BaseVirtualMachineTests.cs b/Strict.Runtime.Tests/BaseVirtualMachineTests.cs similarity index 79% rename from Strict.VirtualMachine.Tests/BaseVirtualMachineTests.cs rename to Strict.Runtime.Tests/BaseVirtualMachineTests.cs index fe2e096a..b0e999c1 100644 --- a/Strict.VirtualMachine.Tests/BaseVirtualMachineTests.cs +++ b/Strict.Runtime.Tests/BaseVirtualMachineTests.cs @@ -5,9 +5,10 @@ namespace Strict.Runtime.Tests; public class BaseVirtualMachineTests : TestExpressions { //ncrunch: no coverage start - protected static readonly Type NumberType = TestPackage.Instance.FindType(Base.Number)!; - protected static readonly Type TextType = TestPackage.Instance.FindType(Base.Text)!; - protected static readonly Type ListType = TestPackage.Instance.FindType(Base.List)!; + protected static readonly Type NumberType = TestPackage.Instance.GetType(Type.Number); + protected static readonly Type ListType = TestPackage.Instance.GetType(Type.List); + protected static ValueInstance Number(double value) => new(NumberType, value); + protected static ValueInstance Text(string value) => new(value); protected static readonly string[] ArithmeticFunctionExample = [ "has First Number", @@ -27,11 +28,11 @@ public class BaseVirtualMachineTests : TestExpressions ]; protected static readonly Statement[] ExpectedStatementsOfArithmeticFunctionExample = [ - new StoreVariableStatement(new Instance(NumberType, 10), "First"), - new StoreVariableStatement(new Instance(NumberType, 5), "Second"), - new StoreVariableStatement(new Instance(TextType, "add"), "operation"), + new StoreVariableStatement(Number(10), "First"), + new StoreVariableStatement(Number(5), "Second"), + new StoreVariableStatement(Text("add"), "operation"), new LoadVariableToRegister(Register.R0, "operation"), - new LoadConstantStatement(Register.R1, new Instance(TextType, "add")), + new LoadConstantStatement(Register.R1, Text("add")), new Binary(Instruction.Equal, Register.R0, Register.R1), new JumpToId(Instruction.JumpToIdIfFalse, 0), new LoadVariableToRegister(Register.R2, "First"), @@ -39,7 +40,7 @@ public class BaseVirtualMachineTests : TestExpressions new Binary(Instruction.Add, Register.R2, Register.R3, Register.R4), new Return(Register.R4), new JumpToId(Instruction.JumpEnd, 0), new LoadVariableToRegister(Register.R5, "operation"), - new LoadConstantStatement(Register.R6, new Instance(TextType, "subtract")), + new LoadConstantStatement(Register.R6, Text("subtract")), new Binary(Instruction.Equal, Register.R5, Register.R6), new JumpToId(Instruction.JumpToIdIfFalse, 1), new LoadVariableToRegister(Register.R7, "First"), @@ -47,7 +48,7 @@ public class BaseVirtualMachineTests : TestExpressions new Binary(Instruction.Subtract, Register.R7, Register.R8, Register.R9), new Return(Register.R9), new JumpToId(Instruction.JumpEnd, 1), new LoadVariableToRegister(Register.R10, "operation"), - new LoadConstantStatement(Register.R11, new Instance(TextType, "multiply")), + new LoadConstantStatement(Register.R11, Text("multiply")), new Binary(Instruction.Equal, Register.R10, Register.R11), new JumpToId(Instruction.JumpToIdIfFalse, 2), new LoadVariableToRegister(Register.R12, "First"), @@ -55,7 +56,7 @@ public class BaseVirtualMachineTests : TestExpressions new Binary(Instruction.Multiply, Register.R12, Register.R13, Register.R14), new Return(Register.R14), new JumpToId(Instruction.JumpEnd, 2), new LoadVariableToRegister(Register.R15, "operation"), - new LoadConstantStatement(Register.R0, new Instance(TextType, "divide")), + new LoadConstantStatement(Register.R0, Text("divide")), new Binary(Instruction.Equal, Register.R15, Register.R0), new JumpToId(Instruction.JumpToIdIfFalse, 3), new LoadVariableToRegister(Register.R1, "First"), @@ -151,29 +152,29 @@ public class BaseVirtualMachineTests : TestExpressions ]; protected static readonly Statement[] ExpectedSimpleMethodCallCode = [ - new StoreVariableStatement(new Instance(NumberType, 2), "firstNumber"), - new StoreVariableStatement(new Instance(NumberType, 5), "secondNumber"), + new StoreVariableStatement(Number(2), "firstNumber"), + new StoreVariableStatement(Number(5), "secondNumber"), new Invoke(Register.R0, null!, null!), new Return(Register.R0) ]; protected static readonly Statement[] ExpectedStatementsOfRemoveParenthesesKata = [ - new StoreVariableStatement(new Instance(TextType, "some(thing)"), "text"), - new StoreVariableStatement(new Instance(TextType, ""), "result"), - new StoreVariableStatement(new Instance(NumberType, 0), "count"), + new StoreVariableStatement(Text("some(thing)"), "text"), + new StoreVariableStatement(Text(""), "result"), + new StoreVariableStatement(Number(0), "count"), new LoadVariableToRegister(Register.R0, "text"), new LoopBeginStatement(Register.R0), new LoadVariableToRegister(Register.R1, "value"), - new LoadConstantStatement(Register.R2, new Instance(TextType, "(")), + new LoadConstantStatement(Register.R2, Text("(")), new Binary(Instruction.Equal, Register.R1, Register.R2), new JumpToId(Instruction.JumpToIdIfFalse, 0), new LoadVariableToRegister(Register.R3, "count"), - new LoadConstantStatement(Register.R4, new Instance(NumberType, 1)), + new LoadConstantStatement(Register.R4, Number(1)), new Binary(Instruction.Add, Register.R3, Register.R4, Register.R5), new StoreFromRegisterStatement(Register.R5, "count"), new JumpToId(Instruction.JumpEnd, 0), new LoadVariableToRegister(Register.R6, "count"), - new LoadConstantStatement(Register.R7, new Instance(NumberType, 0)), + new LoadConstantStatement(Register.R7, Number(0)), new Binary(Instruction.Equal, Register.R6, Register.R7), new JumpToId(Instruction.JumpToIdIfFalse, 1), new LoadVariableToRegister(Register.R8, "result"), @@ -182,11 +183,11 @@ public class BaseVirtualMachineTests : TestExpressions new StoreFromRegisterStatement(Register.R10, "result"), new JumpToId(Instruction.JumpEnd, 1), new LoadVariableToRegister(Register.R11, "value"), - new LoadConstantStatement(Register.R12, new Instance(TextType, ")")), + new LoadConstantStatement(Register.R12, Text(")")), new Binary(Instruction.Equal, Register.R11, Register.R12), new JumpToId(Instruction.JumpToIdIfFalse, 2), new LoadVariableToRegister(Register.R13, "count"), - new LoadConstantStatement(Register.R14, new Instance(NumberType, 1)), + new LoadConstantStatement(Register.R14, Number(1)), new Binary(Instruction.Subtract, Register.R13, Register.R14, Register.R15), new StoreFromRegisterStatement(Register.R15, "count"), new JumpToId(Instruction.JumpEnd, 2), @@ -200,16 +201,9 @@ public class BaseVirtualMachineTests : TestExpressions ]; protected static readonly Statement[] ExpectedStatementsOfSimpleListDeclaration = [ - new StoreVariableStatement(new Instance(NumberType, 5), "number"), - new LoadConstantStatement(Register.R0, new Instance(ListType, - new List - { - new Value(NumberType, 1), - new Value(NumberType, 2), - new Value(NumberType, 3), - new Value(NumberType, 4), - new Value(NumberType, 5) - })), + new StoreVariableStatement(Number(5), "number"), + new LoadConstantStatement(Register.R0, new ValueInstance(ListType.GetGenericImplementation(NumberType), + [Number(1), Number(2), Number(3), Number(4), Number(5)])), new Return(Register.R0) ]; protected static readonly string[] InvertValueKata = @@ -224,20 +218,13 @@ public class BaseVirtualMachineTests : TestExpressions protected static readonly Statement[] ExpectedStatementsOfInvertValueKata = [ new StoreVariableStatement( - new Instance(ListType, - new List - { - new Value(NumberType, 1), - new Value(NumberType, 2), - new Value(NumberType, 3), - new Value(NumberType, 4), - new Value(NumberType, 5) - }), "numbers"), - new StoreVariableStatement(new Instance(TextType, ""), "result"), + new ValueInstance(ListType.GetGenericImplementation(NumberType), + [Number(1), Number(2), Number(3), Number(4)]), "numbers"), + new StoreVariableStatement(Text(""), "result"), new LoadVariableToRegister(Register.R0, "numbers"), new LoopBeginStatement(Register.R0), new LoadVariableToRegister(Register.R1, "value"), - new LoadConstantStatement(Register.R2, new Instance(NumberType, -1)), + new LoadConstantStatement(Register.R2, Number(-1)), new Binary(Instruction.Multiply, Register.R1, Register.R2, Register.R3), new LoadVariableToRegister(Register.R4, "result"), new Binary(Instruction.Add, Register.R4, Register.R3, Register.R5), diff --git a/Strict.VirtualMachine.Tests/BytecodeGeneratorTests.cs b/Strict.Runtime.Tests/BytecodeGeneratorTests.cs similarity index 78% rename from Strict.VirtualMachine.Tests/BytecodeGeneratorTests.cs rename to Strict.Runtime.Tests/BytecodeGeneratorTests.cs index 8192d65f..b8fc9040 100644 --- a/Strict.VirtualMachine.Tests/BytecodeGeneratorTests.cs +++ b/Strict.Runtime.Tests/BytecodeGeneratorTests.cs @@ -20,14 +20,14 @@ private static IEnumerable ByteCodeCases { yield return new TestCaseData("Test(5).Assign", "Test", new Statement[] { - new StoreVariableStatement(new Instance(NumberType, 5), "number"), - new StoreVariableStatement(new Instance(NumberType, 5), "five"), + new StoreVariableStatement(Number(5), "number"), + new StoreVariableStatement(Number(5), "five"), new LoadVariableToRegister(Register.R0, "five"), - new LoadConstantStatement(Register.R1, new Instance(NumberType, 5)), + new LoadConstantStatement(Register.R1, Number(5)), new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2), new StoreFromRegisterStatement(Register.R2, "something"), new LoadVariableToRegister(Register.R3, "something"), - new LoadConstantStatement(Register.R4, new Instance(NumberType, 10)), + new LoadConstantStatement(Register.R4, Number(10)), new Binary(Instruction.Add, Register.R3, Register.R4, Register.R5), new Return(Register.R5) }, @@ -42,8 +42,8 @@ private static IEnumerable ByteCodeCases yield return new TestCaseData("Add(10, 5).Calculate", "Add", new Statement[] { - new StoreVariableStatement(new Instance(NumberType, 10), "First"), - new StoreVariableStatement(new Instance(NumberType, 5), "Second"), + new StoreVariableStatement(Number(10), "First"), + new StoreVariableStatement(Number(5), "Second"), new LoadVariableToRegister(Register.R0, "First"), new LoadVariableToRegister(Register.R1, "Second"), new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2), @@ -60,12 +60,12 @@ private static IEnumerable ByteCodeCases yield return new TestCaseData("AddOne(10, 5).Calculate", "AddOne", new Statement[] { - new StoreVariableStatement(new Instance(NumberType, 10), "First"), - new StoreVariableStatement(new Instance(NumberType, 5), "Second"), + new StoreVariableStatement(Number(10), "First"), + new StoreVariableStatement(Number(5), "Second"), new LoadVariableToRegister(Register.R0, "First"), new LoadVariableToRegister(Register.R1, "Second"), new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2), - new LoadConstantStatement(Register.R3, new Instance(NumberType, 1.0)), + new LoadConstantStatement(Register.R3, Number(1.0)), new Binary(Instruction.Add, Register.R2, Register.R3, Register.R4), new Return(Register.R4) }, @@ -80,8 +80,8 @@ private static IEnumerable ByteCodeCases yield return new TestCaseData("Multiply(10).By(2)", "Multiply", new Statement[] { - new StoreVariableStatement(new Instance(NumberType, 10), "number"), - new StoreVariableStatement(new Instance(NumberType, 2), "multiplyBy"), + new StoreVariableStatement(Number(10), "number"), + new StoreVariableStatement(Number(2), "multiplyBy"), new LoadVariableToRegister(Register.R0, "number"), new LoadVariableToRegister(Register.R1, "multiplyBy"), new Binary(Instruction.Multiply, Register.R0, Register.R1, Register.R2), @@ -95,8 +95,8 @@ private static IEnumerable ByteCodeCases yield return new TestCaseData("Bla(10).SomeFunction", "Bla", new Statement[] { - new StoreVariableStatement(new Instance(NumberType, 10), "number"), - new StoreVariableStatement(new Instance(NumberType, 5), "blaa"), + new StoreVariableStatement(Number(10), "number"), + new StoreVariableStatement(Number(5), "blaa"), new LoadVariableToRegister(Register.R0, "blaa"), new LoadVariableToRegister(Register.R1, "number"), new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2), @@ -106,9 +106,9 @@ private static IEnumerable ByteCodeCases "SimpleLoopExample", new Statement[] { - new StoreVariableStatement(new Instance(NumberType, 10), "number"), - new StoreVariableStatement(new Instance(NumberType, 1), "result"), - new StoreVariableStatement(new Instance(NumberType, 2), "multiplier"), + new StoreVariableStatement(Number(10), "number"), + new StoreVariableStatement(Number(1), "result"), + new StoreVariableStatement(Number(2), "multiplier"), new LoadVariableToRegister(Register.R0, "number"), new LoopBeginStatement(Register.R0), new LoadVariableToRegister(Register.R1, "result"), @@ -138,16 +138,16 @@ private static IEnumerable ByteCodeCases yield return new TestCaseData("IfWithMethodCallLeft(5).Check", "IfWithMethodCallLeft", new Statement[] { - new StoreVariableStatement(new Instance(NumberType, 5), "number"), + new StoreVariableStatement(Number(5), "number"), new Invoke(Register.R0, null!, null!), - new LoadConstantStatement(Register.R1, new Instance(NumberType, 0)), + new LoadConstantStatement(Register.R1, Number(0)), new Binary(Instruction.GreaterThan, Register.R0, Register.R1), new JumpToId(Instruction.JumpToIdIfFalse, 0), new LoadConstantStatement(Register.R2, - new Instance(TestPackage.Instance.GetType(Base.Boolean), true)), + new ValueInstance(TestPackage.Instance.GetType(Type.Boolean), 1)), new Return(Register.R2), new JumpToId(Instruction.JumpEnd, 0), new LoadConstantStatement(Register.R3, - new Instance(TestPackage.Instance.GetType(Base.Boolean), false)), + new ValueInstance(TestPackage.Instance.GetType(Type.Boolean), 0)), new Return(Register.R3) }, new[] @@ -157,4 +157,4 @@ private static IEnumerable ByteCodeCases }); } } -} \ No newline at end of file +} diff --git a/Strict.Runtime.Tests/BytecodeInterpreterTests.cs b/Strict.Runtime.Tests/BytecodeInterpreterTests.cs new file mode 100644 index 00000000..365f5a1d --- /dev/null +++ b/Strict.Runtime.Tests/BytecodeInterpreterTests.cs @@ -0,0 +1,527 @@ +using System.Globalization; + +namespace Strict.Runtime.Tests; + +public class BytecodeInterpreterTests : BaseVirtualMachineTests +{ + [SetUp] + public void Setup() => vm = new BytecodeInterpreter(TestPackage.Instance); + + protected BytecodeInterpreter vm = null!; + + private void CreateSampleEnum() + { + if (type.Package.FindDirectType("Days") == null) + new Type(type.Package, + new TypeLines("Days", "constant Monday = 1", "constant Tuesday = 2", + "constant Wednesday = 3", "constant Friday = 5")). + ParseMembersAndMethods(new MethodExpressionParser()); + } + + [Test] + public void ReturnEnum() + { + CreateSampleEnum(); + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(ReturnEnum), + nameof(ReturnEnum) + "(5).GetMonday", "has dummy Number", "GetMonday Number", + "\tDays.Monday")).Generate(); + var result = vm.Execute(statements).Returns; + Assert.That(result!.Value.Number, Is.EqualTo(1)); + } + + [Test] + public void EnumIfConditionComparison() + { + CreateSampleEnum(); + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource( + nameof(EnumIfConditionComparison), + nameof(EnumIfConditionComparison) + "(5).GetMonday(Days.Monday)", "has dummy Number", + "GetMonday(days) Boolean", "\tif days is Days.Monday", "\t\treturn true", "\telse", + "\t\treturn false")).Generate(); + var result = vm.Execute(statements).Returns!; + Assert.That(result.Value.Number, Is.EqualTo(1)); + } + + [TestCase(Instruction.Add, 15, 5, 10)] + [TestCase(Instruction.Subtract, 5, 8, 3)] + [TestCase(Instruction.Multiply, 4, 2, 2)] + [TestCase(Instruction.Divide, 3, 7.5, 2.5)] + [TestCase(Instruction.Modulo, 1, 5, 2)] + [TestCase(Instruction.Add, "510", "5", 10)] + [TestCase(Instruction.Add, "510", 5, "10")] + [TestCase(Instruction.Add, "510", "5", "10")] + public void Execute(Instruction operation, object expected, params object[] inputs) + { + var result = vm.Execute(BuildStatements(inputs, operation)).Memory.Registers[Register.R1]; + var actual = expected is string + ? (object)result.Text + : result.Number; + Assert.That(actual, Is.EqualTo(expected)); + } + + private static Statement[] + BuildStatements(IReadOnlyList inputs, Instruction operation) => + [ + new SetStatement(inputs[0] is string s0 + ? Text(s0) + : Number(Convert.ToDouble(inputs[0])), Register.R0), + new SetStatement(inputs[1] is string s1 + ? Text(s1) + : inputs[1] is double d + ? Number(d) + : Number(Convert.ToDouble(inputs[1])), Register.R1), + new Binary(operation, Register.R0, Register.R1) + ]; + + [Test] + public void LoadVariable() => + Assert.That(vm.Execute([ + new LoadConstantStatement(Register.R0, Number(5)) + ]).Memory.Registers[Register.R0].Number, Is.EqualTo(5)); + + [Test] + public void SetAndAdd() => + Assert.That(vm.Execute([ + new LoadConstantStatement(Register.R0, Number(10)), + new LoadConstantStatement(Register.R1, Number(5)), + new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2) + ]).Memory.Registers[Register.R2].Number, Is.EqualTo(15)); + + [Test] + public void AddFiveTimes() => + Assert.That(vm.Execute([ + new SetStatement(Number(5), Register.R0), + new SetStatement(Number(1), Register.R1), + new SetStatement(Number(0), Register.R2), + new Binary(Instruction.Add, Register.R0, Register.R2, Register.R2), + new Binary(Instruction.Subtract, Register.R0, Register.R1, Register.R0), + new JumpIfNotZero(-3, Register.R0) + ]).Memory.Registers[Register.R2].Number, Is.EqualTo(0 + 5 + 4 + 3 + 2 + 1)); + + [TestCase("ArithmeticFunction(10, 5).Calculate(\"add\")", 15)] + [TestCase("ArithmeticFunction(10, 5).Calculate(\"subtract\")", 5)] + [TestCase("ArithmeticFunction(10, 5).Calculate(\"multiply\")", 50)] + [TestCase("ArithmeticFunction(10, 5).Calculate(\"divide\")", 2)] + public void RunArithmeticFunctionExample(string methodCall, int expectedResult) + { + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource("ArithmeticFunction", + methodCall, ArithmeticFunctionExample)).Generate(); + Assert.That(vm.Execute(statements).Returns!.Value.Number, Is.EqualTo(expectedResult)); + } + + [Test] + public void AccessListByIndex() + { + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(AccessListByIndex), + nameof(AccessListByIndex) + "(1, 2, 3, 4, 5).Get(2)", "has numbers", + "Get(index Number) Number", "\tnumbers(index)")).Generate(); + Assert.That(vm.Execute(statements).Returns!.Value.Number, Is.EqualTo(3)); + } + + [Test] + public void AccessListByIndexNonNumberType() + { + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource( + nameof(AccessListByIndexNonNumberType), + nameof(AccessListByIndexNonNumberType) + "(\"1\", \"2\", \"3\", \"4\", \"5\").Get(2)", + "has texts", "Get(index Number) Text", "\ttexts(index)")).Generate(); + Assert.That(vm.Execute(statements).Returns!.Value.Text, Is.EqualTo("3")); + } + + [Test] + public void ReduceButGrowLoopExample() => + Assert.That(vm.Execute([ + new StoreVariableStatement(Number(10), "number"), + new StoreVariableStatement(Number(1), "result"), + new StoreVariableStatement(Number(2), "multiplier"), + new LoadVariableToRegister(Register.R0, "number"), + new LoopBeginStatement(Register.R0), new LoadVariableToRegister(Register.R2, "result"), + new LoadVariableToRegister(Register.R3, "multiplier"), + new Binary(Instruction.Multiply, Register.R2, Register.R3, Register.R4), + new StoreFromRegisterStatement(Register.R4, "result"), + new LoopEndStatement(5), + new LoadVariableToRegister(Register.R5, "result"), new Return(Register.R5) + ]).Returns!.Value.Number, Is.EqualTo(1024)); + + [TestCase("NumberConvertor", "NumberConvertor(5).ConvertToText", "5", "has number", + "ConvertToText Text", "\t5 to Text")] + [TestCase("TextConvertor", "TextConvertor(\"5\").ConvertToNumber", 5, "has text", + "ConvertToNumber Number", "\ttext to Number")] + public void ExecuteToOperator(string programName, string methodCall, object expected, + params string[] code) + { + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(programName, + methodCall, code)).Generate(); + var result = vm.Execute(statements).Returns!.Value; + var actual = expected is string + ? (object)result.Text + : result.Number; + Assert.That(actual, Is.EqualTo(expected)); + } + + //ncrunch: no coverage start + private static IEnumerable MethodCallTests + { + get + { + yield return new TestCaseData("AddNumbers", "AddNumbers(2, 5).GetSum", SimpleMethodCallCode, + 7); + yield return new TestCaseData("CallWithConstants", "CallWithConstants(2, 5).GetSum", + MethodCallWithConstantValues, 6); + yield return new TestCaseData("CallWithoutArguments", "CallWithoutArguments(2, 5).GetSum", + MethodCallWithLocalWithNoArguments, 542); + yield return new TestCaseData("CurrentlyFailing", "CurrentlyFailing(10).SumEvenNumbers", + CurrentlyFailingTest, 20); + } + } //ncrunch: no coverage end + + [TestCaseSource(nameof(MethodCallTests))] + // ReSharper disable TooManyArguments, makes below tests easier + public void MethodCall(string programName, string methodCall, string[] source, object expected) + { + var statements = + new ByteCodeGenerator(GenerateMethodCallFromSource(programName, methodCall, source)). + Generate(); + Assert.That(vm.Execute(statements).Returns!.Value.Number, Is.EqualTo(expected)); + } + + [Test] + public void IfAndElseTest() + { + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource("IfAndElseTest", + "IfAndElseTest(3).IsEven", IfAndElseTestCode)).Generate(); + Assert.That(vm.Execute(statements).Returns!.Value.Text, + Is.EqualTo("Number is less or equal than 10")); + } + + [TestCase("EvenSumCalculator(100).IsEven", 2450, "EvenSumCalculator", + new[] + { + "has number", "IsEven Number", "\tmutable sum = 0", "\tfor number", + "\t\tif index % 2 is 0", "\t\t\tsum = sum + index", "\tsum" + })] + [TestCase("EvenSumCalculatorForList(100, 200, 300).IsEvenList", 2, "EvenSumCalculatorForList", + new[] + { + "has numbers", "IsEvenList Number", "\tmutable sum = 0", "\tfor numbers", + "\t\tif index % 2 is 0", "\t\t\tsum = sum + index", "\tsum" + })] + public void CompileCompositeBinariesInIfCorrectlyWithModulo(string methodCall, + object expectedResult, string methodName, params string[] code) + { + var statements = + new ByteCodeGenerator(GenerateMethodCallFromSource(methodName, methodCall, code)). + Generate(); + Assert.That(vm.Execute(statements).Returns!.Value.Number, Is.EqualTo(expectedResult)); + } + + [TestCase("AddToTheList(5).Add", "100 200 300 400 0 1 2 3", "AddToTheList", + new[] + { + "has number", "Add Numbers", "\tmutable myList = (100, 200, 300, 400)", "\tfor myList", + "\t\tif value % 2 is 0", "\t\t\tmyList = myList + index", "\tmyList" + })] + [TestCase("RemoveFromTheList(5).Remove", "100 200 300", "RemoveFromTheList", + new[] + { + "has number", "Remove Numbers", "\tmutable myList = (100, 200, 300, 400)", "\tfor myList", + "\t\tif value is 400", "\t\t\tmyList = myList - 400", "\tmyList" + })] + [TestCase("RemoveB(\"s\", \"b\", \"s\").Remove", "s s", "RemoveB", + new[] + { + "has texts", "Remove Texts", "\tmutable textList = texts", "\tfor texts", + "\t\tif value is \"b\"", "\t\t\ttextList = textList - value", "\ttextList" + })] + [TestCase("ListRemove(\"s\", \"b\", \"s\").Remove", "s s", "ListRemove", + new[] + { + "has texts", "Remove Texts", "\tmutable textList = texts", "\ttextList.Remove(\"b\")", + "\ttextList" + })] + [TestCase("ListRemoveMultiple(\"s\", \"b\", \"s\").Remove", "b", "ListRemoveMultiple", + new[] + { + "has texts", "Remove Texts", "\tmutable textList = texts", "\ttextList.Remove(\"s\")", + "\ttextList" + })] + public void ExecuteListBinaryOperations(string methodCall, object expectedResult, + string programName, params string[] code) + { + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(programName, + methodCall, code)).Generate(); + var result = vm.Execute(statements).Returns!.Value; + var elements = result.List.Items.Aggregate("", (current, item) => current + (item.IsText + ? item.Text + : item.Number) + " "); + Assert.That(elements.Trim(), Is.EqualTo(expectedResult)); + } //ncrunch: no coverage end + + [TestCase("TestContains(\"s\", \"b\", \"s\").Contains(\"b\")", "true", "TestContains", + new[] + { + "has elements Texts", "Contains(other Text) Boolean", "\tfor elements", + "\t\tif value is other", "\t\t\treturn true", "\tfalse" + })] + public void CallCommonMethodCalls(string methodCall, object expectedResult, string programName, + params string[] code) + { + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(programName, + methodCall, code)).Generate(); + var result = vm.Execute(statements).Returns!.Value; + Assert.That(result.ToExpressionCodeString(), Is.EqualTo(expectedResult)); + } + + [TestCase("NumbersAdder(5).AddNumberToList", "1 2 3 5", "has number", "AddNumberToList Numbers", + "\tmutable numbers = (1, 2, 3)", "\tnumbers.Add(number)", "\tnumbers")] + public void CollectionAdd(string methodCall, string expected, params string[] code) + { + var statements = + new ByteCodeGenerator(GenerateMethodCallFromSource("NumbersAdder", methodCall, code)). + Generate(); + var result = ExpressionListToSpaceSeparatedString(statements); + Assert.That(result.TrimEnd(), Is.EqualTo(expected)); + } + + private string ExpressionListToSpaceSeparatedString(IList statements) + { + var result = vm.Execute(statements).Returns!.Value; + return result.List.Items.Aggregate("", (current, item) => current + (item.IsText + ? item.Text + : item.Number) + " "); + } + + [Test] + public void DictionaryAdd() + { + string[] code = + [ + "has number", + "RemoveFromDictionary Number", + "\tmutable values = Dictionary(Number, Number)", "\tvalues.Add(1, number)", "\tnumber" + ]; + Assert.That( + vm.Execute(new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(DictionaryAdd), + "DictionaryAdd(5).RemoveFromDictionary", code)).Generate()).Memory.Variables["values"]. + GetDictionaryItems().Count, Is.EqualTo(1)); + } + + [Test] + public void CreateEmptyDictionaryFromConstructor() + { + var dictionaryType = TestPackage.Instance.GetType(Type.Dictionary). + GetGenericImplementation(NumberType, NumberType); + var methodCall = CreateFromMethodCall(dictionaryType); + var statements = new List { new Invoke(Register.R0, methodCall, new Registry()) }; + var result = vm.Execute(statements).Memory.Registers[Register.R0]; + Assert.That(result.IsDictionary, Is.True); + Assert.That(result.GetDictionaryItems().Count, Is.EqualTo(0)); + } + + [TestCase("DictionaryGet(5).AddToDictionary", "5", "has number", "AddToDictionary Number", + "\tmutable values = Dictionary(Number, Number)", "\tvalues.Add(1, number)", + "\tvalues.Get(1)")] + public void DictionaryGet(string methodCall, string expected, params string[] code) + { + var statements = + new ByteCodeGenerator( + GenerateMethodCallFromSource(nameof(DictionaryGet), methodCall, code)).Generate(); + var result = vm.Execute(statements).Returns!.Value; + var actual = result.IsText + ? result.Text + : result.Number.ToString(CultureInfo.InvariantCulture); + Assert.That(actual, Is.EqualTo(expected)); + } + + [TestCase("DictionaryRemove(5).AddToDictionary", "5", "has number", "AddToDictionary Number", + "\tmutable values = Dictionary(Number, Number)", "\tvalues.Add(1, number)", + "\tvalues.Add(2, number + 10)", "\tvalues.Get(2)")] + public void DictionaryRemove(string methodCall, string expected, params string[] code) + { + var statements = + new ByteCodeGenerator( + GenerateMethodCallFromSource(nameof(DictionaryRemove), methodCall, code)).Generate(); + var result = vm.Execute(statements).Returns!.Value; + var actual = result.IsText + ? result.Text + : result.Number.ToString(CultureInfo.InvariantCulture); + Assert.That(actual, Is.EqualTo("15")); + } + + [Test] + public void ReturnWithinALoop() + { + var source = new[] { "has number", "GetAll Number", "\tfor number", "\t\tvalue" }; + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(ReturnWithinALoop), + "ReturnWithinALoop(5).GetAll", source)).Generate(); + Assert.That(() => vm.Execute(statements).Returns!.Value.Number, + Is.EqualTo(1 + 2 + 3 + 4 + 5)); + } + + [Test] + public void ReverseWithRange() + { + var source = new[] + { + "has numbers", "Reverse Numbers", "\tmutable result = Numbers", + "\tlet len = numbers.Length - 1", "\tfor Range(len, 0)", "\t\tresult.Add(numbers(index))", + "\tresult" + }; + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(ReverseWithRange), + "ReverseWithRange(1, 2, 3).Reverse", source)).Generate(); + Assert.That(() => ExpressionListToSpaceSeparatedString(statements), Is.EqualTo("3 2 1 ")); + } + + [Test] + public void ConditionalJump() => + Assert.That(vm.Execute([ + new SetStatement(Number(5), Register.R0), + new SetStatement(Number(1), Register.R1), + new SetStatement(Number(10), Register.R2), + new Binary(Instruction.LessThan, Register.R2, Register.R0), + new JumpIf(Instruction.JumpIfTrue, 2), + new Binary(Instruction.Add, Register.R2, Register.R0, Register.R0) + ]).Memory.Registers[Register.R0].Number, Is.EqualTo(15)); + + [Test] + public void JumpIfTrueSkipsNextInstruction() => + Assert.That(vm.Execute([ + new SetStatement(Number(1), Register.R0), + new SetStatement(Number(1), Register.R1), + new SetStatement(Number(0), Register.R2), + new Binary(Instruction.Equal, Register.R0, Register.R1), + new JumpIfTrue(1, Register.R0), + new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2) + ]).Memory.Registers[Register.R2].Number, Is.EqualTo(0)); + + [Test] + public void JumpIfFalseSkipsNextInstruction() => + Assert.That(vm.Execute([ + new SetStatement(Number(1), Register.R0), + new SetStatement(Number(2), Register.R1), + new SetStatement(Number(0), Register.R2), + new Binary(Instruction.Equal, Register.R0, Register.R1), + new JumpIfFalse(1, Register.R0), + new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2) + ]).Memory.Registers[Register.R2].Number, Is.EqualTo(0)); + + [TestCase(Instruction.GreaterThan, new[] { 1, 2 }, 2 - 1)] + [TestCase(Instruction.LessThan, new[] { 1, 2 }, 1 + 2)] + [TestCase(Instruction.Equal, new[] { 5, 5 }, 5 + 5)] + [TestCase(Instruction.NotEqual, new[] { 5, 5 }, 5 - 5)] + public void ConditionalJumpIfAndElse(Instruction conditional, int[] registers, int expected) => + Assert.That(vm.Execute([ + new SetStatement(Number(registers[0]), Register.R0), + new SetStatement(Number(registers[1]), Register.R1), + new Binary(conditional, Register.R0, Register.R1), + new JumpIf(Instruction.JumpIfTrue, 2), + new Binary(Instruction.Subtract, Register.R1, Register.R0, Register.R0), + new JumpIf(Instruction.JumpIfFalse, 2), + new Binary(Instruction.Add, Register.R0, Register.R1, Register.R0) + ]).Memory.Registers[Register.R0].Number, Is.EqualTo(expected)); + + [TestCase(Instruction.Add)] + [TestCase(Instruction.GreaterThan)] + public void OperandsRequired(Instruction instruction) => + Assert.That(() => vm.Execute([new Binary(instruction, Register.R0)]), + Throws.InstanceOf()); + + [Test] + public void LoopOverEmptyListSkipsBody() + { + var emptyList = new ValueInstance(NumberType, new List()); + var result = vm.Execute([ + new StoreVariableStatement(emptyList, "numbers"), + new StoreVariableStatement(Number(0), "result"), + new LoadVariableToRegister(Register.R0, "numbers"), + new LoopBeginStatement(Register.R0), new LoadVariableToRegister(Register.R1, "result"), + new LoadConstantStatement(Register.R2, Number(1)), + new Binary(Instruction.Add, Register.R1, Register.R2, Register.R3), + new StoreFromRegisterStatement(Register.R3, "result"), + new LoopEndStatement(5), + new LoadVariableToRegister(Register.R4, "result"), + new Return(Register.R4) + ]).Returns; + Assert.That(result!.Value.Number, Is.EqualTo(0)); + } + + [Test] + public void LoopOverTextStopsWhenIndexExceedsLength() + { + var text = Text("Hi"); + var loopBegin = new LoopBeginStatement(Register.R0); + var result = vm.Execute([ + new StoreVariableStatement(text, "words"), + new StoreVariableStatement(Number(0), "count"), + new LoadVariableToRegister(Register.R0, "words"), + loopBegin, + new LoadVariableToRegister(Register.R1, "count"), + new LoadConstantStatement(Register.R2, Number(1)), + new Binary(Instruction.Add, Register.R1, Register.R2, Register.R3), + new StoreFromRegisterStatement(Register.R3, "count"), + new LoopEndStatement(5), + new LoadVariableToRegister(Register.R4, "count"), + new Return(Register.R4) + ]).Returns; + Assert.That(result!.Value.Number, Is.EqualTo(2)); + } + + [Test] + public void LoopOverListStopsWhenIndexExceedsCount() + { + var source = new[] + { + "has numbers", + "CountItems Number", + "\tmutable count = 0", + "\tfor numbers", + "\t\tcount = count + 1", + "\tcount" + }; + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource( + nameof(LoopOverListStopsWhenIndexExceedsCount), + $"{nameof(LoopOverListStopsWhenIndexExceedsCount)}(1, 2, 3).CountItems", source)).Generate(); + var result = vm.Execute(statements).Returns!.Value.Number; + Assert.That(result, Is.EqualTo(3)); + } + + [Test] + public void LoopOverSingleCharTextStopsAtEnd() + { + var source = new[] + { + "has letter Text", + "CountChars Number", + "\tmutable count = 0", + "\tfor letter", + "\t\tcount = count + 1", + "\tcount" + }; + var statements = new ByteCodeGenerator(GenerateMethodCallFromSource( + nameof(LoopOverSingleCharTextStopsAtEnd), + $"{nameof(LoopOverSingleCharTextStopsAtEnd)}(\"X\").CountChars", source)).Generate(); + var result = vm.Execute(statements).Returns!.Value.Number; + Assert.That(result, Is.EqualTo(1)); + } + + [Test] + public void LoopOverSingleItemListStopsAtEnd() + { + var singleItemList = new ValueInstance(NumberType, + new List { new ValueInstance(NumberType, 42) }); + var result = vm.Execute([ + new StoreVariableStatement(singleItemList, "items"), + new StoreVariableStatement(Number(0), "count"), + new LoadVariableToRegister(Register.R0, "items"), + new LoopBeginStatement(Register.R0), + new LoadVariableToRegister(Register.R1, "count"), + new LoadConstantStatement(Register.R2, Number(1)), + new Binary(Instruction.Add, Register.R1, Register.R2, Register.R3), + new StoreFromRegisterStatement(Register.R3, "count"), + new LoopEndStatement(5), + new LoadVariableToRegister(Register.R4, "count"), + new Return(Register.R4) + ]).Returns; + Assert.That(result!.Value.Number, Is.EqualTo(1)); + } +} \ No newline at end of file diff --git a/Strict.Runtime.Tests/CallFrameTests.cs b/Strict.Runtime.Tests/CallFrameTests.cs new file mode 100644 index 00000000..0fc0797a --- /dev/null +++ b/Strict.Runtime.Tests/CallFrameTests.cs @@ -0,0 +1,51 @@ +namespace Strict.Runtime.Tests; + +public sealed class CallFrameTests +{ + private static readonly ValueInstance SomeNumber = + new(TestPackage.Instance.GetType(Type.Number), 42.0); + + [Test] + public void TryGetReturnsFalseWhenVariableNotFoundInRootFrame() + { + var frame = new CallFrame(); + Assert.That(frame.TryGet("missing", out _), Is.False); + } + + [Test] + public void TryGetReturnsFalseForParentLocalVariableNotExposedAsMember() + { + var parent = new CallFrame(); + parent.Set("local", SomeNumber); + var child = new CallFrame(parent); + Assert.That(child.TryGet("local", out _), Is.False); + } + + [Test] + public void TryGetFindsMemberVariableFromParent() + { + var parent = new CallFrame(); + parent.Set("count", SomeNumber, isMember: true); + var child = new CallFrame(parent); + Assert.That(child.TryGet("count", out _), Is.True); + } + + [Test] + public void ClearRemovesLocalVariables() + { + var frame = new CallFrame(); + frame.Set("x", SomeNumber); + frame.Clear(); + Assert.That(frame.TryGet("x", out _), Is.False); + } + + [Test] + public void ClearRemovesMemberVariablesFromChildVisibility() + { + var parent = new CallFrame(); + parent.Set("count", SomeNumber, isMember: true); + parent.Clear(); + var child = new CallFrame(parent); + Assert.That(child.TryGet("count", out _), Is.False); + } +} diff --git a/Strict.VirtualMachine.Tests/GlobalUsings.cs b/Strict.Runtime.Tests/GlobalUsings.cs similarity index 100% rename from Strict.VirtualMachine.Tests/GlobalUsings.cs rename to Strict.Runtime.Tests/GlobalUsings.cs diff --git a/Strict.Runtime.Tests/MemoryTests.cs b/Strict.Runtime.Tests/MemoryTests.cs new file mode 100644 index 00000000..e5ebbbc8 --- /dev/null +++ b/Strict.Runtime.Tests/MemoryTests.cs @@ -0,0 +1,36 @@ +namespace Strict.Runtime.Tests; + +public sealed class MemoryTests +{ + private static readonly Type NumberType = TestPackage.Instance.GetType(Type.Number); + + [Test] + public void AddToCollectionVariableDoesNothingWhenValueIsNotAList() + { + var memory = new Memory(); + memory.Variables["count"] = new ValueInstance(NumberType, 5.0); + memory.AddToCollectionVariable("count", new ValueInstance(NumberType, 1.0)); + Assert.That(memory.Variables["count"].Number, Is.EqualTo(5)); + } + + [Test] + public void AddToCollectionVariableThrowsWhenEmptyListHasNonGenericType() + { + var memory = new Memory(); + var rawListType = TestPackage.Instance.GetType(Type.List); + memory.Variables["items"] = new ValueInstance(rawListType, new List()); + Assert.That(() => memory.AddToCollectionVariable("items", new ValueInstance(NumberType, 1.0)), + Throws.InstanceOf()); + } + + [Test] + public void AddToCollectionVariableAddsElementToNonEmptyList() + { + var memory = new Memory(); + var listType = TestPackage.Instance.GetListImplementationType(NumberType); + memory.Variables["items"] = new ValueInstance(listType, + new List { new(NumberType, 1.0) }); + memory.AddToCollectionVariable("items", new ValueInstance(NumberType, 2.0)); + Assert.That(memory.Variables["items"].List.Items.Count, Is.EqualTo(2)); + } +} diff --git a/Strict.VirtualMachine.Tests/Strict.Runtime.Tests.csproj b/Strict.Runtime.Tests/Strict.Runtime.Tests.csproj similarity index 88% rename from Strict.VirtualMachine.Tests/Strict.Runtime.Tests.csproj rename to Strict.Runtime.Tests/Strict.Runtime.Tests.csproj index e3388035..5625cb5b 100644 --- a/Strict.VirtualMachine.Tests/Strict.Runtime.Tests.csproj +++ b/Strict.Runtime.Tests/Strict.Runtime.Tests.csproj @@ -11,6 +11,7 @@ + @@ -20,7 +21,7 @@ - + diff --git a/Strict.Runtime.Tests/Strict.Runtime.Tests.v3.ncrunchproject b/Strict.Runtime.Tests/Strict.Runtime.Tests.v3.ncrunchproject new file mode 100644 index 00000000..dd39421f --- /dev/null +++ b/Strict.Runtime.Tests/Strict.Runtime.Tests.v3.ncrunchproject @@ -0,0 +1,12 @@ + + + + + Strict.Runtime.Tests.AdderProgramTests.AddTotalsForThreeNumbers + + + Strict.Runtime.Tests.AdderProgramTests.BenchmarkCompare + + + + \ No newline at end of file diff --git a/Strict.Runtime.Tests/ValueInstanceMigrationTests.cs b/Strict.Runtime.Tests/ValueInstanceMigrationTests.cs new file mode 100644 index 00000000..d92a5860 --- /dev/null +++ b/Strict.Runtime.Tests/ValueInstanceMigrationTests.cs @@ -0,0 +1,13 @@ +namespace Strict.Runtime.Tests; + +public sealed class ValueInstanceMigrationTests : BaseVirtualMachineTests +{ + [Test] + public void StatementsShouldUseValueInstanceNotInstance() + { + var valueInstance = new Strict.Expressions.ValueInstance(NumberType, 42.0); + var statement = new LoadConstantStatement(Register.R0, valueInstance); + Assert.That(statement.ValueInstance, Is.EqualTo(valueInstance)); + Assert.That(statement.ValueInstance.Number, Is.EqualTo(42.0)); + } +} diff --git a/Strict.VirtualMachine.Tests/VirtualMachineKataTests.cs b/Strict.Runtime.Tests/VirtualMachineKataTests.cs similarity index 80% rename from Strict.VirtualMachine.Tests/VirtualMachineKataTests.cs rename to Strict.Runtime.Tests/VirtualMachineKataTests.cs index 4ba9f890..0de2058d 100644 --- a/Strict.VirtualMachine.Tests/VirtualMachineKataTests.cs +++ b/Strict.Runtime.Tests/VirtualMachineKataTests.cs @@ -3,7 +3,7 @@ namespace Strict.Runtime.Tests; public class BytecodeInterpreterKataTests : BaseVirtualMachineTests { [SetUp] - public void Setup() => vm = new BytecodeInterpreter(); + public void Setup() => vm = new BytecodeInterpreter(TestPackage.Instance); protected BytecodeInterpreter vm = null!; @@ -22,7 +22,7 @@ public void BestTimeToBuyStocksKata() "\t\telse if value - min > max", "\t\t\tmax = value - min", "\tmax")).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(5)); + Assert.That(vm.Execute(statements).Returns!.Value.Number, Is.EqualTo(5)); } [TestCase("RemoveParentheses(\"some(thing)\").Remove", "some")] @@ -31,7 +31,7 @@ public void RemoveParentheses(string methodCall, string expectedResult) { var statements = new ByteCodeGenerator(GenerateMethodCallFromSource("RemoveParentheses", methodCall, RemoveParenthesesKata)).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(expectedResult)); + Assert.That(vm.Execute(statements).Returns!.Value.Text, Is.EqualTo(expectedResult)); } [TestCase("Invertor(1, 2, 3, 4, 5).Invert", "-1-2-3-4-5")] @@ -39,7 +39,7 @@ public void InvertValues(string methodCall, string expectedResult) { var statements = new ByteCodeGenerator(GenerateMethodCallFromSource("Invertor", methodCall, InvertValueKata)).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(expectedResult)); + Assert.That(vm.Execute(statements).Returns!.Value.Text, Is.EqualTo(expectedResult)); } [Test] @@ -54,6 +54,6 @@ public void CountingSheepKata() "\t\tif value", "\t\t\tresult.Increment", "\tresult")).Generate(); - Assert.That(vm.Execute(statements).Returns!.Value, Is.EqualTo(17)); + Assert.That(vm.Execute(statements).Returns!.Value.Number, Is.EqualTo(17)); } -} \ No newline at end of file +} diff --git a/Strict.VirtualMachine/BytecodeGenerator.cs b/Strict.Runtime/BytecodeGenerator.cs similarity index 86% rename from Strict.VirtualMachine/BytecodeGenerator.cs rename to Strict.Runtime/BytecodeGenerator.cs index d046d707..2544ddef 100644 --- a/Strict.VirtualMachine/BytecodeGenerator.cs +++ b/Strict.Runtime/BytecodeGenerator.cs @@ -1,4 +1,3 @@ -using System.Collections; using System.Diagnostics; using Strict.Expressions; using Strict.Language; @@ -43,15 +42,25 @@ public ByteCodeGenerator(MethodCall methodCall) } private IReadOnlyList Expressions { get; } - private readonly Type? returnType; + private readonly Type returnType; private int forResultId; - private void AddMembersFromCaller(Instance instance) - { - if (instance.ReturnType != null) - statements.Add(new StoreVariableStatement(instance, - instance.ReturnType.Members.First(member => !member.Type.IsTrait).Name)); - } + private void AddMembersFromCaller(ValueInstance instance) => + statements.Add(new StoreVariableStatement(instance, + instance.GetTypeExceptText().Members.First(member => !member.Type.IsTrait).Name, isMember: true)); + + private static ValueInstance GetValueInstanceFromExpression(Expression expression) => + expression switch + { + List list => list.TryGetConstantData() ?? throw new NotSupportedException( + "Dynamic lists (mutable or containing any non constant expression) are not supported yet"), + Value val => val.Data, + MemberCall memberCall when memberCall.Member.InitialValue != null => + memberCall.Member.InitialValue is Value enumValue + ? enumValue.Data + : new ValueInstance(memberCall.Member.InitialValue.ToString()), + _ => new ValueInstance(expression.ToString()) //ncrunch: no coverage + }; private void AddInstanceMemberVariables(MethodCall instance) { @@ -60,15 +69,20 @@ private void AddInstanceMemberVariables(MethodCall instance) { if (instance.Method.Parameters[parameterIndex].Type is GenericTypeImplementation { - Generic.Name: Base.List - }) + Generic.Name: Type.List + } && !(instance.Arguments.Count == 1 && instance.Arguments[0] is List)) + { + var listItems = instance.Arguments.Select(GetValueInstanceFromExpression).ToList(); statements.Add(new StoreVariableStatement( - new Instance(instance.Method.Parameters[parameterIndex].Type, instance.Arguments), - instance.ReturnType.Members[parameterIndex].Name)); + new ValueInstance(instance.Method.Parameters[parameterIndex].Type, listItems), + instance.ReturnType.Members[parameterIndex].Name, isMember: true)); + } else + { statements.Add(new StoreVariableStatement( - new Instance(instance.Arguments[parameterIndex], true), - instance.ReturnType.Members[parameterIndex].Name)); + GetValueInstanceFromExpression(instance.Arguments[parameterIndex]), + instance.ReturnType.Members[parameterIndex].Name, isMember: true)); + } } } @@ -77,20 +91,20 @@ private void AddMethodParameterVariables(MethodCall methodCall) for (var parameterIndex = 0; parameterIndex < methodCall.Method.Parameters.Count; parameterIndex++) statements?.Add(new StoreVariableStatement( - new Instance(methodCall.Arguments[parameterIndex]), + GetValueInstanceFromExpression(methodCall.Arguments[parameterIndex]), methodCall.Method.Parameters[parameterIndex].Name)); } public List Generate() => GenerateStatements(Expressions); - private List GenerateStatements(IEnumerable expressions) + private List GenerateStatements(IReadOnlyList expressions) { - foreach (var expression in expressions) - if ((expression.GetHashCode() == Expressions[^1].GetHashCode() || - expression is Expressions.Return) && expression is not If) - GenerateStatementsFromReturn(expression); + for (var i = 0; i < expressions.Count; i++) + if ((ReferenceEquals(expressions[i], Expressions[^1]) || + expressions[i] is Expressions.Return) && expressions[i] is not If) + GenerateStatementsFromReturn(expressions[i]); else - GenerateStatementsFromExpression(expression); + GenerateStatementsFromExpression(expressions[i]); return statements; } @@ -113,7 +127,7 @@ private void GenerateStatementsFromReturn(Expression expression) private bool TryGenerateNumberForLoopReturn(Expression expression) { - if (expression is not For forExpression || returnType?.Name != Base.Number) + if (expression is not For forExpression || !returnType.IsNumber) return false; GenerateStatementsForNumberAggregation(forExpression); return true; @@ -122,7 +136,7 @@ private bool TryGenerateNumberForLoopReturn(Expression expression) private void GenerateStatementsForNumberAggregation(For forExpression) { var resultVariable = $"forResult{forResultId++}"; - statements.Add(new StoreVariableStatement(new Instance(returnType!, 0), resultVariable)); + statements.Add(new StoreVariableStatement(new ValueInstance(returnType, 0), resultVariable)); GenerateLoopStatements(forExpression, resultVariable); statements.Add(new LoadVariableToRegister(registry.AllocateRegister(), resultVariable)); statements.Add(new Return(registry.PreviousRegister)); @@ -175,16 +189,20 @@ private void GenerateStatementsFromExpression(Expression expression) private void TryGenerateForEnum(Type type, Expression value) { if (type.IsEnum) - statements.Add(new LoadConstantStatement(registry.AllocateRegister(), - new Instance(type, value))); + { + var data = value is Value val + ? val.Data + : new ValueInstance(value.ToString()); + statements.Add(new LoadConstantStatement(registry.AllocateRegister(), data)); + } } private bool? TryGenerateValueStatement(Expression expression) { - if (expression is not Value valueExpression) + if (expression is not Value) return null; statements.Add(new LoadConstantStatement(registry.AllocateRegister(), - new Instance(valueExpression.ReturnType, valueExpression.Data))); + GetValueInstanceFromExpression(expression))); return true; } @@ -231,7 +249,7 @@ private void GenerateStatementsForRemoveMethod(MethodCall methodCall) if (methodCall.Instance == null) return; //ncrunch: no coverage GenerateStatementsFromExpression(methodCall.Arguments[0]); - if (methodCall.Instance.ReturnType is GenericTypeImplementation { Generic.Name: Base.List }) + if (methodCall.Instance.ReturnType is GenericTypeImplementation { Generic.Name: Type.List }) statements.Add(new RemoveStatement(methodCall.Instance.ToString(), registry.PreviousRegister)); } @@ -305,14 +323,11 @@ private void GenerateForAssignmentOrDeclaration(Expression declarationOrAssignme private void TryGenerateStatementsForAssignmentValue(Value assignmentValue, string variableName) { - var data = assignmentValue.Data switch - { - IEnumerable expressions => expressions.ToList(), - IDictionary => new Dictionary(), - _ => assignmentValue.Data - }; - statements.Add(new StoreVariableStatement(new Instance(assignmentValue.ReturnType, data), - variableName)); + var data = assignmentValue.ReturnType.IsDictionary + ? new ValueInstance(assignmentValue.ReturnType, + new Dictionary()) + : GetValueInstanceFromExpression(assignmentValue); + statements.Add(new StoreVariableStatement(data, variableName)); } private bool? TryGenerateIfStatements(Expression expression) @@ -335,7 +350,7 @@ private void GenerateLoopStatements(For forExpression, string? aggregationTarget { var statementCountBeforeLoopStart = statements.Count; if (forExpression.Iterator is MethodCall rangeExpression && - forExpression.Iterator.ReturnType.Name == Base.Range && + forExpression.Iterator.ReturnType.Name == Type.Range && rangeExpression.Method.Name == Method.From) GenerateStatementForRangeLoopInstruction(rangeExpression); else @@ -428,7 +443,7 @@ private void GenerateForBooleanCallIfCondition(Expression condition) GenerateStatementsFromExpression(condition); var instanceCallRegister = registry.PreviousRegister; statements.Add(new LoadConstantStatement(registry.AllocateRegister(), - new Instance(condition.ReturnType, true))); + new ValueInstance(condition.ReturnType, 1.0))); GenerateInstructionsFromIfCondition(Instruction.Equal, instanceCallRegister, registry.PreviousRegister); } diff --git a/Strict.VirtualMachine/BytecodeInterpreter.cs b/Strict.Runtime/BytecodeInterpreter.cs similarity index 55% rename from Strict.VirtualMachine/BytecodeInterpreter.cs rename to Strict.Runtime/BytecodeInterpreter.cs index 4cdb8f83..feab790f 100644 --- a/Strict.VirtualMachine/BytecodeInterpreter.cs +++ b/Strict.Runtime/BytecodeInterpreter.cs @@ -4,15 +4,17 @@ using Strict.Runtime.Statements; using BinaryStatement = Strict.Runtime.Statements.Binary; using Return = Strict.Runtime.Statements.Return; +using Type = Strict.Language.Type; namespace Strict.Runtime; -public sealed class BytecodeInterpreter +public sealed class BytecodeInterpreter(Package package) { + private readonly Package package = package; + public BytecodeInterpreter Execute(IList allStatements) { Clear(); - // Reset loop state that lives on LoopBeginStatement objects foreach (var loopBegin in allStatements.OfType()) loopBegin.Reset(); return RunStatements(allStatements); @@ -25,14 +27,14 @@ private void Clear() statements.Clear(); Returns = null; Memory.Registers.Clear(); - Memory.Variables.Clear(); + Memory.Frame = new CallFrame(); } private bool conditionFlag; private int instructionIndex; private IList statements = new List(); - public Instance? Returns { get; private set; } - public Memory Memory { get; private init; } = new(); + public ValueInstance? Returns { get; private set; } + public Memory Memory { get; } = new(); private BytecodeInterpreter RunStatements(IList allStatements) { @@ -62,48 +64,44 @@ private void ExecuteStatement(Statement statement) private void TryRemoveStatement(Statement statement) { - if (statement is RemoveStatement removeStatement) - { - var item = Memory.Registers[removeStatement.Register].GetRawValue(); - var list = (List)Memory.Variables[removeStatement.Identifier].Value; - list.RemoveAll(expression => EqualsExtensions.AreEqual(((Value)expression).Data, item)); - } + if (statement is not RemoveStatement removeStatement) + return; + var item = Memory.Registers[removeStatement.Register]; + var listItems = new List(Memory.Frame.Get(removeStatement.Identifier).List.Items); + listItems.RemoveAll(vi => vi.Equals(item)); + Memory.Frame.Set(removeStatement.Identifier, + new ValueInstance(Memory.Frame.Get(removeStatement.Identifier).List.ReturnType, listItems)); } private void TryExecuteListCall(Statement statement) { if (statement is not ListCallStatement listCallStatement) return; - var indexValue = - Convert.ToInt32(Memory.Registers[listCallStatement.IndexValueRegister].GetRawValue()); - var variableListElement = - ((List)Memory.Variables[listCallStatement.Identifier].Value). - ElementAt(indexValue); - Memory.Registers[listCallStatement.Register] = - new Instance(variableListElement.ReturnType, variableListElement); + var indexValue = (int)Memory.Registers[listCallStatement.IndexValueRegister].Number; + var variableListElement = Memory.Frame.Get(listCallStatement.Identifier).List.Items.ElementAt(indexValue); + Memory.Registers[listCallStatement.Register] = variableListElement; } - private void TryWriteToTableInstruction(Statement statement) + private void TryWriteToListInstruction(Statement statement) { - if (statement is not WriteToTableStatement writeToTableStatement) + if (statement is not WriteToListStatement writeToListStatement) return; - Memory.AddToDictionary(writeToTableStatement.Identifier, - Memory.Registers[writeToTableStatement.Key], Memory.Registers[writeToTableStatement.Value]); + Memory.AddToCollectionVariable(writeToListStatement.Identifier, + Memory.Registers[writeToListStatement.Register]); } - private void TryWriteToListInstruction(Statement statement) + private void TryWriteToTableInstruction(Statement statement) { - if (statement is not WriteToListStatement writeToListStatement) + if (statement is not WriteToTableStatement writeToTableStatement) return; - Memory.AddToCollectionVariable(writeToListStatement.Identifier, - Memory.Registers[writeToListStatement.Register].Value); + Memory.AddToDictionary(writeToTableStatement.Identifier, + Memory.Registers[writeToTableStatement.Key], Memory.Registers[writeToTableStatement.Value]); } private void TryLoopEndInstruction(Statement statement) { if (statement is not LoopEndStatement loopEndStatement) return; - // Find the most recently started loop by scanning backward var loopBegin = statements.Take(instructionIndex).OfType().Last(); loopBegin.LoopCount--; if (loopBegin.LoopCount <= 0) @@ -124,60 +122,73 @@ private void TryInvokeInstruction(Statement statement) if (GetValueByKeyForDictionaryAndStoreInRegister(invokeStatement)) return; var methodStatements = GetByteCodeFromInvokedMethodCall(invokeStatement); - var instance = new BytecodeInterpreter - { - Memory = new Memory - { - Registers = Memory.Registers, - Variables = - new Dictionary( - Memory.Variables.Where(variable => variable.Value.IsMember)) - } - }.RunStatements(methodStatements).Returns; - if (instance != null) - Memory.Registers[invokeStatement.Register] = instance; + var result = RunChildScope(methodStatements); + if (result != null) + Memory.Registers[invokeStatement.Register] = result.Value; } /// - /// Handles Number.Increment and Number.Decrement by directly computing the result without - /// going through the generic method invocation path (which fails for primitive types with no members). + /// Runs in a child while reusing + /// this interpreter instance. All mutable fields are saved on the C# call stack (zero heap + /// allocations for the bookkeeping) and restored after the child finishes. /// + private ValueInstance? RunChildScope(IList childStatements) + { + var savedStatements = statements; + var savedIndex = instructionIndex; + var savedConditionFlag = conditionFlag; + var savedReturns = Returns; + var savedFrame = Memory.Frame; + Memory.Frame = new CallFrame(savedFrame); + Returns = null; + RunStatements(childStatements); + var result = Returns; + Memory.Frame.Clear(); + Memory.Frame = savedFrame; + statements = savedStatements; + instructionIndex = savedIndex; + conditionFlag = savedConditionFlag; + Returns = savedReturns; + return result; + } + private bool TryHandleIncrementDecrement(Invoke invoke) { var methodName = invoke.Method?.Method.Name; if (methodName != "Increment" && methodName != "Decrement") return false; if (invoke.Method!.Instance == null || - !Memory.Variables.TryGetValue(invoke.Method.Instance.ToString(), out var current)) + !Memory.Frame.TryGet(invoke.Method.Instance.ToString(), out var current)) return false; //ncrunch: no coverage var delta = methodName == "Increment" - ? 1m - : -1m; + ? 1.0 + : -1.0; Memory.Registers[invoke.Register] = - new Instance(current.ReturnType, Convert.ToDecimal(current.Value) + delta); + new ValueInstance(current.GetTypeExceptText(), current.Number + delta); return true; } - /// - /// Handles 'value to Text' and 'value to Number' method calls via the 'To' expression. - /// private bool TryHandleToConversion(Invoke invoke) { if (invoke.Method?.Method.Name != BinaryOperator.To) return false; var instanceExpr = invoke.Method.Instance ?? throw new InvalidOperationException(); - object rawValue; - if (instanceExpr is Value constValue) - rawValue = constValue.Data; - else if (Memory.Variables.TryGetValue(instanceExpr.ToString(), out var varValue)) - rawValue = varValue.GetRawValue(); - else - throw new InvalidOperationException(); //ncrunch: no coverage + var rawValue = instanceExpr is Value constValue + ? constValue.Data + : Memory.Frame.TryGet(instanceExpr.ToString(), out var varValue) + ? varValue + : throw new InvalidOperationException(); //ncrunch: no coverage var conversionType = invoke.Method.ReturnType; - if (conversionType.Name == Base.Text) - Memory.Registers[invoke.Register] = new Instance(conversionType, rawValue.ToString() ?? ""); - else if (conversionType.Name == Base.Number) - Memory.Registers[invoke.Register] = new Instance(conversionType, Convert.ToDecimal(rawValue)); + if (conversionType.IsText) + Memory.Registers[invoke.Register] = + rawValue.IsText + ? rawValue + : new ValueInstance(rawValue.ToExpressionCodeString()); + else if (conversionType.IsNumber) + Memory.Registers[invoke.Register] = + rawValue.IsText + ? new ValueInstance(conversionType, Convert.ToDouble(rawValue.Text)) + : rawValue; return true; } @@ -186,10 +197,10 @@ private bool TryCreateEmptyDictionaryInstance(Invoke invoke) if (invoke.Method?.Instance != null || invoke.Method?.Method.Name != Method.From || invoke.Method?.ReturnType is not GenericTypeImplementation { - Generic.Name: Base.Dictionary + Generic.Name: Type.Dictionary } dictionaryType) return false; - Memory.Registers[invoke.Register] = new Instance(dictionaryType, new Dictionary()); + Memory.Registers[invoke.Register] = new ValueInstance(dictionaryType, new Dictionary()); return true; } @@ -198,18 +209,18 @@ private bool GetValueByKeyForDictionaryAndStoreInRegister(Invoke invoke) if (invoke.Method?.Method.Name != "Get" || invoke.Method.Instance?.ReturnType is not GenericTypeImplementation { - Generic.Name: Base.Dictionary + Generic.Name: Type.Dictionary }) return false; var keyArg = invoke.Method.Arguments[0]; var keyData = keyArg is Value argValue ? argValue.Data - : Memory.Variables[keyArg.ToString()].Value; - var dictionary = Memory.Variables[invoke.Method.Instance.ToString()].Value; - var value = ((Dictionary)dictionary). - FirstOrDefault(element => EqualsExtensions.AreEqual(element.Key.Data, keyData)).Value; - if (value != null) - Memory.Registers[invoke.Register] = new Instance(value.ReturnType, value); + : Memory.Frame.Get(keyArg.ToString()); + var dictionary = Memory.Frame.Get(invoke.Method.Instance.ToString()); + var value = dictionary.GetDictionaryItems(). + FirstOrDefault(element => element.Key.Equals(keyData)).Value; + if (!Equals(value, default(ValueInstance))) + Memory.Registers[invoke.Register] = value; return true; } @@ -221,8 +232,9 @@ private List GetByteCodeFromInvokedMethodCall(Invoke invoke) new InvokedMethod(GetExpressionsFromMethod(invoke.Method.Method), FormArgumentsForMethodCall(invoke), invoke.Method.Method.ReturnType), invoke.PersistedRegistry).Generate(); - var instance = GetVariableInstanceFromMemory(invoke.Method?.Instance?.ToString() ?? - throw new InvalidOperationException()); + if (!Memory.Frame.TryGet(invoke.Method?.Instance?.ToString() ?? + throw new InvalidOperationException(), out var instance)) + throw new VariableNotFoundInMemory(); //ncrunch: no coverage return new ByteCodeGenerator( new InstanceInvokedMethod(GetExpressionsFromMethod(invoke.Method!.Method), FormArgumentsForMethodCall(invoke), instance, invoke.Method.Method.ReturnType), @@ -237,23 +249,17 @@ private static IReadOnlyList GetExpressionsFromMethod(Method method) : [result]; } - private Instance GetVariableInstanceFromMemory(string variableIdentifier) - { - Memory.Variables.TryGetValue(variableIdentifier, out var methodCallInstance); - return methodCallInstance ?? throw new VariableNotFoundInMemory(); - } - - private Dictionary FormArgumentsForMethodCall(Invoke invoke) + private Dictionary FormArgumentsForMethodCall(Invoke invoke) { - var arguments = new Dictionary(); + var arguments = new Dictionary(); if (invoke.Method == null) return arguments; // ncrunch: no coverage for (var index = 0; index < invoke.Method.Method.Parameters.Count; index++) { var argument = invoke.Method.Arguments[index]; var argumentInstance = argument is Value argumentValue - ? new Instance(argumentValue.ReturnType, argumentValue.Data) - : Memory.Variables[argument.ToString()]; + ? argumentValue.Data + : Memory.Frame.Get(argument.ToString()); arguments.Add(invoke.Method.Method.Parameters[index].Name, argumentInstance); } return arguments; @@ -264,8 +270,6 @@ private bool TryExecuteReturn(Statement statement) if (statement is not Return returnStatement) return false; Returns = Memory.Registers[returnStatement.Register]; - if (!Returns.Value.GetType().IsPrimitive && Returns.Value is not Value) - return false; instructionIndex = -2; return true; } @@ -282,62 +286,81 @@ private void TryLoopInitInstruction(Statement statement) private void ProcessCollectionLoopIteration(LoopBeginStatement loopBeginStatement) { - if (Memory.Variables.ContainsKey("index")) - Memory.Variables["index"].Value = Convert.ToInt32(Memory.Variables["index"].Value) + 1; - else - Memory.Variables.Add("index", new Instance(Base.Number, 0)); - Memory.Registers.TryGetValue(loopBeginStatement.Register, out var iterableVariable); - if (iterableVariable is null) + if (!Memory.Registers.TryGet(loopBeginStatement.Register, out var iterableVariable)) return; //ncrunch: no coverage + if (Memory.Frame.ContainsKey("index")) + { + var current = Memory.Frame.Get("index"); + Memory.Frame.Set("index", new ValueInstance(package.GetType(Type.Number), current.Number + 1)); + } + else + Memory.Frame.Set("index", new ValueInstance(package.GetType(Type.Number), 0)); if (!loopBeginStatement.IsInitialized) { loopBeginStatement.LoopCount = GetLength(iterableVariable); loopBeginStatement.IsInitialized = true; } - AlterValueVariable(iterableVariable); + AlterValueVariable(iterableVariable, package.GetType(Type.Number), loopBeginStatement); + if (loopBeginStatement.LoopCount <= 0) + { + var stepsToLoopEnd = statements.Skip(instructionIndex + 1). + TakeWhile(s => s is not LoopEndStatement).Count(); + instructionIndex += stepsToLoopEnd; + } } private void ProcessRangeLoopIteration(LoopBeginStatement loopBeginStatement) { if (!loopBeginStatement.IsInitialized) { - var startIndex = Convert.ToInt32(Memory.Registers[loopBeginStatement.Register].Value); - var endIndex = Convert.ToInt32(Memory.Registers[loopBeginStatement.EndIndex!.Value].Value); + var startIndex = Convert.ToInt32(Memory.Registers[loopBeginStatement.Register].Number); + var endIndex = Convert.ToInt32(Memory.Registers[loopBeginStatement.EndIndex!.Value].Number); loopBeginStatement.InitializeRangeState(startIndex, endIndex); } var isDecreasing = loopBeginStatement.IsDecreasing ?? false; - if (Memory.Variables.ContainsKey("index")) - Memory.Variables["index"].Value = Convert.ToInt32(Memory.Variables["index"].Value) + - (isDecreasing - ? -1 - : 1); + var numberType = Memory.Registers[loopBeginStatement.Register].GetTypeExceptText().GetType(Type.Number); + if (Memory.Frame.ContainsKey("index")) + { + var current = Memory.Frame.Get("index"); + Memory.Frame.Set("index", new ValueInstance(numberType, current.Number + (isDecreasing + ? -1 + : 1))); + } else - Memory.Variables.Add("index", - new Instance(Base.Number, loopBeginStatement.StartIndexValue ?? 0)); - Memory.Variables["value"] = Memory.Variables["index"]; + Memory.Frame.Set("index", + new ValueInstance(numberType, loopBeginStatement.StartIndexValue ?? 0)); + Memory.Frame.Set("value", Memory.Frame.Get("index")); } - private static int GetLength(Instance iterableInstance) + private static int GetLength(ValueInstance iterableInstance) { - if (iterableInstance.Value is string iterableString) - return iterableString.Length; - if (iterableInstance.Value is int or double) - return Convert.ToInt32(iterableInstance.Value); - return iterableInstance.ReturnType is { IsIterator: true } - ? ((IEnumerable)iterableInstance.Value).Count() - : 0; //ncrunch: no coverage + if (iterableInstance.IsText) + return iterableInstance.Text.Length; + if (iterableInstance.IsList) + return iterableInstance.List.Items.Count; + return (int)iterableInstance.Number; } - private void AlterValueVariable(Instance iterableVariable) + private void AlterValueVariable(ValueInstance iterableVariable, Type numberType, + LoopBeginStatement loopBeginStatement) { - var index = Convert.ToInt32(Memory.Variables["index"].Value); - var value = iterableVariable.Value.ToString(); - if (iterableVariable.ReturnType?.Name == Base.Text && value is not null) - Memory.Variables["value"] = new Instance(Base.Text, value[index].ToString()); - else if (iterableVariable.ReturnType is GenericTypeImplementation { Generic.Name: Base.List }) - Memory.Variables["value"] = new Instance(((List)iterableVariable.Value)[index]); - else if (iterableVariable.ReturnType?.Name == Base.Number) - Memory.Variables["value"] = new Instance(Base.Number, index + 1); + var index = Convert.ToInt32(Memory.Frame.Get("index").Number); + if (iterableVariable.IsText) + { + if (index < iterableVariable.Text.Length) + Memory.Frame.Set("value", new ValueInstance(iterableVariable.Text[index].ToString())); + return; + } + if (iterableVariable.IsList) + { + var items = iterableVariable.List.Items; + if (index < items.Count) + Memory.Frame.Set("value", items[index]); + else + loopBeginStatement.LoopCount = 0; + return; + } + Memory.Frame.Set("value", new ValueInstance(numberType, index + 1)); } private void TryStoreInstructions(Statement statement) @@ -345,21 +368,22 @@ private void TryStoreInstructions(Statement statement) if (statement.Instruction > Instruction.StoreSeparator) return; if (statement is SetStatement setStatement) - Memory.Registers[setStatement.Register] = setStatement.Instance; + Memory.Registers[setStatement.Register] = setStatement.ValueInstance; else if (statement is StoreVariableStatement storeVariableStatement) - Memory.Variables[storeVariableStatement.Identifier] = storeVariableStatement.Instance; + Memory.Frame.Set(storeVariableStatement.Identifier, storeVariableStatement.ValueInstance, + storeVariableStatement.IsMember); else if (statement is StoreFromRegisterStatement storeFromRegisterStatement) - Memory.Variables[storeFromRegisterStatement.Identifier] = - Memory.Registers[storeFromRegisterStatement.Register]; + Memory.Frame.Set(storeFromRegisterStatement.Identifier, + Memory.Registers[storeFromRegisterStatement.Register]); } private void TryLoadInstructions(Statement statement) { if (statement is LoadVariableToRegister loadVariableStatement) Memory.Registers[loadVariableStatement.Register] = - Memory.Variables[loadVariableStatement.Identifier]; + Memory.Frame.Get(loadVariableStatement.Identifier); else if (statement is LoadConstantStatement loadConstantStatement) - Memory.Registers[loadConstantStatement.Register] = loadConstantStatement.Instance; + Memory.Registers[loadConstantStatement.Register] = loadConstantStatement.ValueInstance; } private void TryExecuteRest(Statement statement) @@ -384,44 +408,65 @@ private void TryBinaryOperationExecution(BinaryStatement statement) var (right, left) = GetOperands(statement); Memory.Registers[statement.Registers[^1]] = statement.Instruction switch { - Instruction.Add => left + right, - Instruction.Subtract => left - right, - Instruction.Multiply => new Instance(right.ReturnType, - Convert.ToDouble(left.Value) * Convert.ToDouble(right.Value)), - Instruction.Divide => new Instance(right.ReturnType, - Convert.ToDouble(left.Value) / Convert.ToDouble(right.Value)), - Instruction.Modulo => new Instance(right.ReturnType, - Convert.ToDouble(left.Value) % Convert.ToDouble(right.Value)), + Instruction.Add => AddValueInstances(left, right), + Instruction.Subtract => SubtractValueInstances(left, right), + Instruction.Multiply => new ValueInstance(right.GetTypeExceptText(), + left.Number * right.Number), + Instruction.Divide => new ValueInstance(right.GetTypeExceptText(), + left.Number / right.Number), + Instruction.Modulo => new ValueInstance(right.GetTypeExceptText(), + left.Number % right.Number), _ => Memory.Registers[statement.Registers[^1]] //ncrunch: no coverage }; } - private (Instance, Instance) GetOperands(BinaryStatement statement) => - Memory.Registers.Count < 2 + private static ValueInstance AddValueInstances(ValueInstance left, ValueInstance right) + { + if (left.IsList) + { + var items = new List(left.List.Items) { right }; + return new ValueInstance(left.List.ReturnType, items); + } + if (left.IsText || right.IsText) + return new ValueInstance((left.IsText + ? left.Text + : left.Number.ToString()) + (right.IsText + ? right.Text + : right.Number.ToString())); + return new ValueInstance(right.GetTypeExceptText(), left.Number + right.Number); + } + + private static ValueInstance SubtractValueInstances(ValueInstance left, ValueInstance right) + { + if (left.IsList) + { + var items = new List(left.List.Items); + var removeIndex = items.FindIndex(item => item.Equals(right)); + if (removeIndex >= 0) + items.RemoveAt(removeIndex); + return new ValueInstance(left.List.ReturnType, items); + } + return new ValueInstance(left.GetTypeExceptText(), left.Number - right.Number); + } + + private (ValueInstance, ValueInstance) GetOperands(BinaryStatement statement) => + statement.Registers.Length < 2 ? throw new OperandsRequired() : (Memory.Registers[statement.Registers[1]], Memory.Registers[statement.Registers[0]]); private void TryConditionalOperationExecution(BinaryStatement statement) { var (right, left) = GetOperands(statement); - NormalizeValues(right, left); conditionFlag = statement.Instruction switch { - Instruction.GreaterThan => left > right, - Instruction.LessThan => left < right, - Instruction.Equal => EqualsExtensions.AreEqual(left.GetRawValue(), right.GetRawValue()), - Instruction.NotEqual => !EqualsExtensions.AreEqual(left.GetRawValue(), right.GetRawValue()), + Instruction.GreaterThan => left.Number > right.Number, + Instruction.LessThan => left.Number < right.Number, + Instruction.Equal => left.Equals(right), + Instruction.NotEqual => !left.Equals(right), _ => false //ncrunch: no coverage }; } - private static void NormalizeValues(params Instance[] instances) - { - foreach (var instance in instances) - if (instance.Value is MemberCall member && member.Member.InitialValue != null) - instance.Value = member.Member.InitialValue; - } - private void TryJumpOperation(Jump statement) { if (conditionFlag && statement.Instruction is Instruction.JumpIfTrue || @@ -434,7 +479,7 @@ private void TryJumpIfOperation(JumpIf statement) if (conditionFlag && statement.Instruction is Instruction.JumpIfTrue || !conditionFlag && statement.Instruction is Instruction.JumpIfFalse || statement is JumpIfNotZero jumpIfNotZeroStatement && - Convert.ToInt32(Memory.Registers[jumpIfNotZeroStatement.Register].Value) > 0) + Memory.Registers[jumpIfNotZeroStatement.Register].Number > 0) instructionIndex += Convert.ToInt32(statement.Steps); } diff --git a/Strict.Runtime/CallFrame.cs b/Strict.Runtime/CallFrame.cs new file mode 100644 index 00000000..3e566a0a --- /dev/null +++ b/Strict.Runtime/CallFrame.cs @@ -0,0 +1,74 @@ +using Strict.Expressions; + +namespace Strict.Runtime; + +/// +/// Variable scope for one method invocation. Writes always go to this frame's own lazy dict; +/// reads walk the parent chain but only through member variables, so child calls can access +/// the caller's 'has' fields without copying them. Modelled after +/// . +/// +internal sealed class CallFrame(CallFrame? parent = null) +{ + private Dictionary? variables; + private HashSet? memberNames; + + /// + /// Materialised locals dict — used by for test compat. + /// + internal Dictionary Variables => + variables ??= new Dictionary(); + + internal bool TryGet(string name, out ValueInstance value) + { + if (variables != null && variables.TryGetValue(name, out value)) + return true; + if (parent != null) + return parent.TryGetMember(name, out value); + value = default; + return false; + } + + private bool TryGetMember(string name, out ValueInstance value) + { + if (memberNames != null && memberNames.Contains(name) && + variables != null && variables.TryGetValue(name, out value)) + return true; + value = default; + return false; + } + + internal ValueInstance Get(string name) => + TryGet(name, out var value) ? value : throw new KeyNotFoundException(name); + + /// + /// Always writes to this frame's own dict (never clobbers parent). + /// + internal void Set(string name, ValueInstance value, bool isMember = false) + { + variables ??= new Dictionary(); + variables[name] = value; + if (isMember) + { + memberNames ??= []; + memberNames.Add(name); + } + } + + internal bool ContainsKey(string name) + { + if (variables != null && variables.ContainsKey(name)) + return true; + return parent != null && parent.ContainsKeyAsMember(name); + } + + private bool ContainsKeyAsMember(string name) => + memberNames != null && memberNames.Contains(name) && + variables != null && variables.ContainsKey(name); + + internal void Clear() + { + variables?.Clear(); + memberNames?.Clear(); + } +} diff --git a/Strict.VirtualMachine/InstanceInvokedMethod.cs b/Strict.Runtime/InstanceInvokedMethod.cs similarity index 54% rename from Strict.VirtualMachine/InstanceInvokedMethod.cs rename to Strict.Runtime/InstanceInvokedMethod.cs index a94eaf64..3b769acd 100644 --- a/Strict.VirtualMachine/InstanceInvokedMethod.cs +++ b/Strict.Runtime/InstanceInvokedMethod.cs @@ -1,11 +1,12 @@ +using Strict.Expressions; using Strict.Language; using Type = Strict.Language.Type; namespace Strict.Runtime; public sealed class InstanceInvokedMethod(IReadOnlyList expressions, - IReadOnlyDictionary arguments, Instance instanceCall, Type returnType) : + IReadOnlyDictionary arguments, ValueInstance instanceCall, Type returnType) : InvokedMethod(expressions, arguments, returnType) { - public Instance InstanceCall { get; } = instanceCall; + public ValueInstance InstanceCall { get; } = instanceCall; } \ No newline at end of file diff --git a/Strict.VirtualMachine/Instruction.cs b/Strict.Runtime/Instruction.cs similarity index 95% rename from Strict.VirtualMachine/Instruction.cs rename to Strict.Runtime/Instruction.cs index c93d13f4..08a44e04 100644 --- a/Strict.VirtualMachine/Instruction.cs +++ b/Strict.Runtime/Instruction.cs @@ -1,4 +1,4 @@ -namespace Strict.Runtime; +namespace Strict.Runtime; /// /// Each Instruction corresponds to a Statement class in the namespace @@ -36,5 +36,4 @@ public enum Instruction ControlFlowSeparator = 400, Invoke, Return -} - +} \ No newline at end of file diff --git a/Strict.VirtualMachine/InvokedMethod.cs b/Strict.Runtime/InvokedMethod.cs similarity index 60% rename from Strict.VirtualMachine/InvokedMethod.cs rename to Strict.Runtime/InvokedMethod.cs index c23101ee..f03c4d61 100644 --- a/Strict.VirtualMachine/InvokedMethod.cs +++ b/Strict.Runtime/InvokedMethod.cs @@ -1,12 +1,13 @@ +using Strict.Expressions; using Strict.Language; using Type = Strict.Language.Type; namespace Strict.Runtime; public class InvokedMethod(IReadOnlyList expressions, - IReadOnlyDictionary arguments, Type returnType) + IReadOnlyDictionary arguments, Type returnType) { public IReadOnlyList Expressions { get; } = expressions; - public IReadOnlyDictionary Arguments { get; } = arguments; + public IReadOnlyDictionary Arguments { get; } = arguments; public Type ReturnType { get; } = returnType; } \ No newline at end of file diff --git a/Strict.Runtime/Memory.cs b/Strict.Runtime/Memory.cs new file mode 100644 index 00000000..5eb8a938 --- /dev/null +++ b/Strict.Runtime/Memory.cs @@ -0,0 +1,52 @@ +using Strict.Expressions; +using Strict.Language; + +namespace Strict.Runtime; + +public sealed class Memory +{ + /// + /// Array-backed register file — O(1) access with no hashing overhead. + /// + public RegisterFile Registers { get; } = new(); + + /// + /// Current variable scope; replaced via call stack. + /// + internal CallFrame Frame { get; set; } = new(); + + /// + /// Exposes the current frame's local variable dict for backward-compatible test access. + /// Use methods for scoped lookup inside the interpreter. + /// + public Dictionary Variables => Frame.Variables; + + public void AddToCollectionVariable(string key, ValueInstance element) + { + Frame.TryGet(key, out var collection); + if (!collection.IsList) + return; + var listItems = new List(collection.List.Items); + if (listItems.Count > 0) + { + listItems.Add(element); + Frame.Set(key, new ValueInstance(collection.List.ReturnType, listItems)); + return; + } + if (collection.GetTypeExceptText() is not GenericTypeImplementation genericImplementationType) + throw new InvalidOperationException(); + listItems.Add(element); + Frame.Set(key, new ValueInstance(genericImplementationType, listItems)); + } + + public void AddToDictionary(string variableKey, ValueInstance keyToAddTo, ValueInstance value) + { + Frame.TryGet(variableKey, out var collection); + if (collection.IsDictionary) + Frame.Set(variableKey, new ValueInstance(collection.GetTypeExceptText(), + new Dictionary(collection.GetDictionaryItems()) + { + { keyToAddTo, value } + })); + } +} \ No newline at end of file diff --git a/Strict.VirtualMachine/Register.cs b/Strict.Runtime/Register.cs similarity index 100% rename from Strict.VirtualMachine/Register.cs rename to Strict.Runtime/Register.cs diff --git a/Strict.Runtime/RegisterFile.cs b/Strict.Runtime/RegisterFile.cs new file mode 100644 index 00000000..04db594f --- /dev/null +++ b/Strict.Runtime/RegisterFile.cs @@ -0,0 +1,28 @@ +using Strict.Expressions; + +namespace Strict.Runtime; + +/// +/// Fixed-size array-backed register file for the 16 slots. +/// Replaces Dictionary<Register, ValueInstance>: array indexing is O(1) with no +/// hash overhead and a single allocation instead of the dictionary's internal bucket arrays. +/// +public sealed class RegisterFile +{ + private readonly ValueInstance[] data = new ValueInstance[16]; + + public ValueInstance this[Register r] + { + get => data[(int)r]; + set => data[(int)r] = value; + } + + /// Returns false (and a default value) only when the slot has never been written. + internal bool TryGet(Register r, out ValueInstance value) + { + value = data[(int)r]; + return !EqualityComparer.Default.Equals(value, default); + } + + public void Clear() => Array.Clear(data, 0, 16); +} diff --git a/Strict.VirtualMachine/Registry.cs b/Strict.Runtime/Registry.cs similarity index 100% rename from Strict.VirtualMachine/Registry.cs rename to Strict.Runtime/Registry.cs diff --git a/Strict.VirtualMachine/Statements/Binary.cs b/Strict.Runtime/Statements/Binary.cs similarity index 100% rename from Strict.VirtualMachine/Statements/Binary.cs rename to Strict.Runtime/Statements/Binary.cs diff --git a/Strict.Runtime/Statements/InstanceStatement.cs b/Strict.Runtime/Statements/InstanceStatement.cs new file mode 100644 index 00000000..fb816a32 --- /dev/null +++ b/Strict.Runtime/Statements/InstanceStatement.cs @@ -0,0 +1,9 @@ +using Strict.Expressions; + +namespace Strict.Runtime.Statements; + +public abstract class InstanceStatement(Instruction instruction, ValueInstance valueInstance) : Statement(instruction) +{ + public ValueInstance ValueInstance { get; } = valueInstance; + public override string ToString() => $"{Instruction} {ValueInstance.ToExpressionCodeString()}"; +} \ No newline at end of file diff --git a/Strict.VirtualMachine/Statements/Invoke.cs b/Strict.Runtime/Statements/Invoke.cs similarity index 100% rename from Strict.VirtualMachine/Statements/Invoke.cs rename to Strict.Runtime/Statements/Invoke.cs diff --git a/Strict.VirtualMachine/Statements/IterationEnd.cs b/Strict.Runtime/Statements/IterationEnd.cs similarity index 100% rename from Strict.VirtualMachine/Statements/IterationEnd.cs rename to Strict.Runtime/Statements/IterationEnd.cs diff --git a/Strict.VirtualMachine/Statements/Jump.cs b/Strict.Runtime/Statements/Jump.cs similarity index 100% rename from Strict.VirtualMachine/Statements/Jump.cs rename to Strict.Runtime/Statements/Jump.cs diff --git a/Strict.VirtualMachine/Statements/JumpIf.cs b/Strict.Runtime/Statements/JumpIf.cs similarity index 100% rename from Strict.VirtualMachine/Statements/JumpIf.cs rename to Strict.Runtime/Statements/JumpIf.cs diff --git a/Strict.VirtualMachine/Statements/JumpIfFalse.cs b/Strict.Runtime/Statements/JumpIfFalse.cs similarity index 100% rename from Strict.VirtualMachine/Statements/JumpIfFalse.cs rename to Strict.Runtime/Statements/JumpIfFalse.cs diff --git a/Strict.VirtualMachine/Statements/JumpIfNotZero.cs b/Strict.Runtime/Statements/JumpIfNotZero.cs similarity index 100% rename from Strict.VirtualMachine/Statements/JumpIfNotZero.cs rename to Strict.Runtime/Statements/JumpIfNotZero.cs diff --git a/Strict.VirtualMachine/Statements/JumpIfTrue.cs b/Strict.Runtime/Statements/JumpIfTrue.cs similarity index 100% rename from Strict.VirtualMachine/Statements/JumpIfTrue.cs rename to Strict.Runtime/Statements/JumpIfTrue.cs diff --git a/Strict.VirtualMachine/Statements/JumpToId.cs b/Strict.Runtime/Statements/JumpToId.cs similarity index 100% rename from Strict.VirtualMachine/Statements/JumpToId.cs rename to Strict.Runtime/Statements/JumpToId.cs diff --git a/Strict.VirtualMachine/Statements/ListCallStatement.cs b/Strict.Runtime/Statements/ListCallStatement.cs similarity index 100% rename from Strict.VirtualMachine/Statements/ListCallStatement.cs rename to Strict.Runtime/Statements/ListCallStatement.cs diff --git a/Strict.VirtualMachine/Statements/LoadConstantStatement.cs b/Strict.Runtime/Statements/LoadConstantStatement.cs similarity index 78% rename from Strict.VirtualMachine/Statements/LoadConstantStatement.cs rename to Strict.Runtime/Statements/LoadConstantStatement.cs index 36e03255..4e26728f 100644 --- a/Strict.VirtualMachine/Statements/LoadConstantStatement.cs +++ b/Strict.Runtime/Statements/LoadConstantStatement.cs @@ -1,10 +1,12 @@ +using Strict.Expressions; + namespace Strict.Runtime.Statements; /// /// Loads a constant value to one of the s, which could be an actual /// number, a boolean or a pointer to some memory (usually an offset). /// -public sealed class LoadConstantStatement(Register register, Instance constant) +public sealed class LoadConstantStatement(Register register, ValueInstance constant) : InstanceStatement(Instruction.LoadConstantToRegister, constant) { public Register Register { get; } = register; diff --git a/Strict.VirtualMachine/Statements/LoadVariableToRegister.cs b/Strict.Runtime/Statements/LoadVariableToRegister.cs similarity index 100% rename from Strict.VirtualMachine/Statements/LoadVariableToRegister.cs rename to Strict.Runtime/Statements/LoadVariableToRegister.cs diff --git a/Strict.Runtime/Statements/LoopBeginStatement.cs b/Strict.Runtime/Statements/LoopBeginStatement.cs index e69de29b..ccbdf6c0 100644 --- a/Strict.Runtime/Statements/LoopBeginStatement.cs +++ b/Strict.Runtime/Statements/LoopBeginStatement.cs @@ -0,0 +1,43 @@ +namespace Strict.Runtime.Statements; + +public sealed class LoopBeginStatement : RegisterStatement +{ + public LoopBeginStatement(Register register) : base(Instruction.LoopBegin, register) { } + + /// Range loop: from startIndex to endIndex register (inclusive). + public LoopBeginStatement(Register startIndex, Register endIndex) + : base(Instruction.LoopBegin, startIndex) + { + EndIndex = endIndex; + IsRange = true; + } + + public Register? EndIndex { get; } + public bool IsRange { get; } + /// Loop execution state - set during Execute, reset before each new run. + public bool IsInitialized { get; set; } + public int LoopCount { get; set; } + public int? StartIndexValue { get; private set; } + public int? EndIndexValue { get; private set; } + public bool? IsDecreasing { get; private set; } + + public void InitializeRangeState(int startIndex, int endIndex) + { + StartIndexValue = startIndex; + EndIndexValue = endIndex; + IsDecreasing = endIndex < startIndex; + LoopCount = (IsDecreasing.Value + ? startIndex - endIndex + : endIndex - startIndex) + 1; + IsInitialized = true; + } + + public void Reset() + { + IsInitialized = false; + LoopCount = 0; + StartIndexValue = null; + EndIndexValue = null; + IsDecreasing = null; + } +} diff --git a/Strict.VirtualMachine/Statements/RegisterStatement.cs b/Strict.Runtime/Statements/RegisterStatement.cs similarity index 100% rename from Strict.VirtualMachine/Statements/RegisterStatement.cs rename to Strict.Runtime/Statements/RegisterStatement.cs diff --git a/Strict.VirtualMachine/Statements/RemoveStatement.cs b/Strict.Runtime/Statements/RemoveStatement.cs similarity index 100% rename from Strict.VirtualMachine/Statements/RemoveStatement.cs rename to Strict.Runtime/Statements/RemoveStatement.cs diff --git a/Strict.VirtualMachine/Statements/Return.cs b/Strict.Runtime/Statements/Return.cs similarity index 100% rename from Strict.VirtualMachine/Statements/Return.cs rename to Strict.Runtime/Statements/Return.cs diff --git a/Strict.Runtime/Statements/SetStatement.cs b/Strict.Runtime/Statements/SetStatement.cs new file mode 100644 index 00000000..d9f56b03 --- /dev/null +++ b/Strict.Runtime/Statements/SetStatement.cs @@ -0,0 +1,10 @@ +using Strict.Expressions; + +namespace Strict.Runtime.Statements; + +public sealed class SetStatement(ValueInstance valueInstance, Register register) + : InstanceStatement(Instruction.Set, valueInstance) +{ + public Register Register { get; } = register; + public override string ToString() => $"{base.ToString()} {Register}"; +} diff --git a/Strict.VirtualMachine/Statements/Statement.cs b/Strict.Runtime/Statements/Statement.cs similarity index 100% rename from Strict.VirtualMachine/Statements/Statement.cs rename to Strict.Runtime/Statements/Statement.cs diff --git a/Strict.VirtualMachine/Statements/StoreFromRegisterStatement.cs b/Strict.Runtime/Statements/StoreFromRegisterStatement.cs similarity index 100% rename from Strict.VirtualMachine/Statements/StoreFromRegisterStatement.cs rename to Strict.Runtime/Statements/StoreFromRegisterStatement.cs diff --git a/Strict.VirtualMachine/Statements/StoreVariableStatement.cs b/Strict.Runtime/Statements/StoreVariableStatement.cs similarity index 56% rename from Strict.VirtualMachine/Statements/StoreVariableStatement.cs rename to Strict.Runtime/Statements/StoreVariableStatement.cs index 4bce27b3..7b721788 100644 --- a/Strict.VirtualMachine/Statements/StoreVariableStatement.cs +++ b/Strict.Runtime/Statements/StoreVariableStatement.cs @@ -1,8 +1,11 @@ +using Strict.Expressions; + namespace Strict.Runtime.Statements; -public sealed class StoreVariableStatement(Instance constant, string identifier) +public sealed class StoreVariableStatement(ValueInstance constant, string identifier, bool isMember = false) : InstanceStatement(Instruction.StoreConstantToVariable, constant) { public string Identifier { get; } = identifier; + public bool IsMember { get; } = isMember; public override string ToString() => $"{base.ToString()} {Identifier}"; } \ No newline at end of file diff --git a/Strict.VirtualMachine/Statements/WriteToListStatement.cs b/Strict.Runtime/Statements/WriteToListStatement.cs similarity index 100% rename from Strict.VirtualMachine/Statements/WriteToListStatement.cs rename to Strict.Runtime/Statements/WriteToListStatement.cs diff --git a/Strict.VirtualMachine/Statements/WriteToTableStatement.cs b/Strict.Runtime/Statements/WriteToTableStatement.cs similarity index 100% rename from Strict.VirtualMachine/Statements/WriteToTableStatement.cs rename to Strict.Runtime/Statements/WriteToTableStatement.cs diff --git a/Strict.VirtualMachine/Strict.Runtime.csproj b/Strict.Runtime/Strict.Runtime.csproj similarity index 85% rename from Strict.VirtualMachine/Strict.Runtime.csproj rename to Strict.Runtime/Strict.Runtime.csproj index 5b4a3231..eb2d88a0 100644 --- a/Strict.VirtualMachine/Strict.Runtime.csproj +++ b/Strict.Runtime/Strict.Runtime.csproj @@ -14,4 +14,8 @@ + + + + \ No newline at end of file diff --git a/Strict.TestRunner.Tests/Program.cs b/Strict.TestRunner.Tests/Program.cs new file mode 100644 index 00000000..65a78fa0 --- /dev/null +++ b/Strict.TestRunner.Tests/Program.cs @@ -0,0 +1,21 @@ +using Strict.TestRunner.Tests; + +//ncrunch: no coverage start +var tests = new TestExecutorTests(); +// Warm up, will cache a lot of things: first parse, types, bodies, expressions +tests.RunAllTestsInPackage(); +Console.WriteLine("Initial warmup run: " + tests.executor.Statistics); +tests.executor.Statistics.Reset(); +var allocatedBefore = GC.GetAllocatedBytesForCurrentThread(); +var startTicks = DateTime.UtcNow.Ticks; +const int Runs = 100; +for (var count = 0; count < Runs; count++) + tests.RunAllTestsInPackage(); +var endTicks = DateTime.UtcNow.Ticks; +var allocatedAfter = GC.GetAllocatedBytesForCurrentThread(); +Console.WriteLine("Total execution time per run: " + + TimeSpan.FromTicks(endTicks - startTicks) / Runs); +Console.WriteLine("Allocated bytes per run: " + (allocatedAfter - allocatedBefore) / Runs); +tests.executor.Statistics.Reset(); +tests.RunAllTestsInPackage(); +Console.WriteLine("One run: " + tests.executor.Statistics); \ No newline at end of file diff --git a/Strict.TestRunner.Tests/Strict.TestRunner.Tests.csproj b/Strict.TestRunner.Tests/Strict.TestRunner.Tests.csproj index b628779e..2bf2732f 100644 --- a/Strict.TestRunner.Tests/Strict.TestRunner.Tests.csproj +++ b/Strict.TestRunner.Tests/Strict.TestRunner.Tests.csproj @@ -6,13 +6,14 @@ enable enable false + false - + + - diff --git a/Strict.TestRunner.Tests/Strict.TestRunner.Tests.v3.ncrunchproject b/Strict.TestRunner.Tests/Strict.TestRunner.Tests.v3.ncrunchproject new file mode 100644 index 00000000..15e6cb16 --- /dev/null +++ b/Strict.TestRunner.Tests/Strict.TestRunner.Tests.v3.ncrunchproject @@ -0,0 +1,12 @@ + + + + + Strict.TestRunner.Tests.TestExecutorTests.BenchmarkCompare + + + Strict.TestRunner.Tests.TestExecutorTests.RunAllTestsInPackageTwice + + + + \ No newline at end of file diff --git a/Strict.TestRunner.Tests/TestExecutorTests.cs b/Strict.TestRunner.Tests/TestExecutorTests.cs index 58017f13..007df9ed 100644 --- a/Strict.TestRunner.Tests/TestExecutorTests.cs +++ b/Strict.TestRunner.Tests/TestExecutorTests.cs @@ -1,3 +1,6 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Running; using Strict.Expressions; using Strict.HighLevelRuntime; using Strict.Language; @@ -6,12 +9,11 @@ namespace Strict.TestRunner.Tests; -public sealed class TestExecutorTests +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Throughput, warmupCount: 1, iterationCount: 10)] +public class TestExecutorTests { - [SetUp] - public void Setup() => executor = new TestExecutor(); - - private TestExecutor executor = null!; + public readonly TestExecutor executor = new(TestPackage.Instance); [Test] public void RunMethod() @@ -36,7 +38,7 @@ public void RunMethodWithFailingTest() " 10")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(() => executor.RunMethod(type.Methods.First(m => m.Name == "Run")), Throws.InstanceOf().With.InnerException.With.Message. - StartsWith("\"Run\" method failed: 5 is 6, result: False")); + Contains("\"Run\" method failed: 5 is 6, result: Boolean: false")); } [Test] @@ -68,7 +70,7 @@ public void RunAllTestsInTypeWithFailure() " 5")).ParseMembersAndMethods(new MethodExpressionParser()); Assert.That(() => executor.RunAllTestsInType(type), Throws.InstanceOf().With.InnerException.With.Message. - Contains("\"Other\" method failed: 2 is 3, result: False")); + Contains("\"Other\" method failed: 2 is 3, result: Boolean: false")); } [Test] @@ -123,7 +125,7 @@ public void RunNumberToCharacterBody() "13 to Character is canOnlyConvertSingleDigit", "value is in Range(0, 10) then Character(Character.zeroCharacter + value) else canOnlyConvertSingleDigit(value)" // @formatter:on - }.ToWordList(Environment.NewLine))); + }.ToLines())); executor.RunAllTestsInType(type); } @@ -147,7 +149,7 @@ public void RunRangeSum() "for range", "\tvalue" // @formatter:on - }.ToWordList(Environment.NewLine))); + }.ToLines())); executor.RunAllTestsInType(type); } @@ -167,7 +169,7 @@ public void RunListLength() "for numbers", "\t1" // @formatter:on - }.ToWordList(Environment.NewLine))); + }.ToLines())); executor.RunAllTestsInType(type); } @@ -221,5 +223,27 @@ public void CompareMutableList() } [Test] + public void RunDictionaryTestsTwice() + { + using var type = TestPackage.Instance.GetType(Type.Dictionary); + executor.RunAllTestsInType(type); + executor.RunAllTestsInType(type); + } + + [Test] + [Benchmark] public void RunAllTestsInPackage() => executor.RunAllTestsInPackage(TestPackage.Instance); + + //ncrunch: no coverage start + [Test] + [Category("Slow")] + public void RunAllTestsInPackageTwice() + { + executor.RunAllTestsInPackage(TestPackage.Instance); + executor.RunAllTestsInPackage(TestPackage.Instance); + } + + [Test] + [Category("Manual")] + public void BenchmarkCompare() => BenchmarkRunner.Run(); } \ No newline at end of file diff --git a/Strict.TestRunner/TestExecutor.cs b/Strict.TestRunner/TestExecutor.cs index d4cf47b6..62eef086 100644 --- a/Strict.TestRunner/TestExecutor.cs +++ b/Strict.TestRunner/TestExecutor.cs @@ -9,22 +9,27 @@ namespace Strict.TestRunner; /// we don't call some code, it is not parsed, executed, or tested at all. This forces execution /// of every method in every type to run all included tests to find out if anything is not working. /// -public sealed class TestExecutor +public sealed class TestExecutor(Package package) : Executor(package, TestBehavior.TestRunner) { - private readonly Executor executor = new(TestBehavior.TestRunner); - public void RunAllTestsInPackage(Package package) { - foreach (var type in package) - RunAllTestsInType(type); + Statistics.PackagesTested++; + foreach (var type in new List(package.Types.Values)) + if (type is not GenericTypeImplementation) + RunAllTestsInType(type); } public void RunAllTestsInType(Type type) { + Statistics.TypesTested++; foreach (var method in type.Methods) if (!method.IsTrait) RunMethod(method); } - public void RunMethod(Method method) => executor.Execute(method, null, []); + public void RunMethod(Method method) + { + Statistics.MethodsTested++; + Execute(method); + } } \ No newline at end of file diff --git a/Strict.Tokens.Tests/LineLexerTests.cs b/Strict.Tokens.Tests/LineLexerTests.cs deleted file mode 100644 index 3b47c80b..00000000 --- a/Strict.Tokens.Tests/LineLexerTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; - -namespace Strict.Tokens.Tests -{ - public class LineLexerTests : Tokenizer - { - [SetUp] - public void CreateLexer() - { - lineLexer = new LineLexer(this); - tokens.Clear(); - } - - private LineLexer lineLexer; - private readonly List tokens = new List(); - public void Add(DefinitionToken token) => tokens.Add(token); - - [Test] - public void EveryLineMustStartWithTab() => - Assert.Throws(() => lineLexer.Process("test")); - - [Test] - public void MultipleSpacesAreNotAllowed() => - Assert.Throws(() => lineLexer.Process(" ")); - - [Test] - public void FindSingleToken() - { - CheckSingleToken(" test(", DefinitionToken.Open); - CheckSingleToken(" test(number)", DefinitionToken.Close); - /*unused - CheckSingleToken(" is", DefinitionToken.Is); - CheckSingleToken(" test", DefinitionToken.Test); - */ - CheckSingleToken(" 53", DefinitionToken.FromNumber(53)); - CheckSingleToken(" number", DefinitionToken.FromIdentifier("number")); - } - - private void CheckSingleToken(string line, DefinitionToken expectedLastToken) - { - tokens.Clear(); - lineLexer.Process(line); - Assert.That(tokens.Last(), Is.EqualTo(expectedLastToken)); - } - - [Test] - public void NumbersInIdentifiersAreNotAllowed() => - Assert.Throws(() => lineLexer.Process(" let abc1")); - - [Test] - public void AllUpperCaseIdentifiersAreNotAllowed() => - Assert.Throws(() => lineLexer.Process(" let AAA")); - /*nah - [Test] - public void ProcessLine() - { - lineLexer.Process(" test(1) is 2"); - Assert.That(lineLexer.Tabs, Is.EqualTo(1)); - Assert.That(tokens, - Is.EqualTo(new List - { - DefinitionToken.Test, - DefinitionToken.Open, - DefinitionToken.FromNumber(1), - DefinitionToken.Close, - DefinitionToken.Is, - DefinitionToken.FromNumber(2) - })); - } - - [Test] - public void ProcessMultipleLines() - { - lineLexer.Process(" test(1) is 2"); - lineLexer.Process(" let doubled = number + number"); - lineLexer.Process(" return doubled"); - Assert.That(lineLexer.Tabs, Is.EqualTo(1)); - Assert.That(tokens, - Is.EqualTo(new List - { - DefinitionToken.Test, - DefinitionToken.Open, - DefinitionToken.FromNumber(1), - DefinitionToken.Close, - DefinitionToken.Is, - DefinitionToken.FromNumber(2), - DefinitionToken.Let, - DefinitionToken.FromIdentifier("doubled"), - DefinitionToken.Assign, - DefinitionToken.FromIdentifier("number"), - DefinitionToken.Plus, - DefinitionToken.FromIdentifier("number"), - DefinitionToken.Return, - DefinitionToken.FromIdentifier("doubled") - })); - } - - [Test] - public void ProcessMethodCall() - { - lineLexer.Process(" log.WriteLine(\"Hey\")"); - Assert.That(tokens, - Is.EqualTo(new List - { - DefinitionToken.FromIdentifier("log"), - DefinitionToken.Dot, - DefinitionToken.FromIdentifier("WriteLine"), - DefinitionToken.Open, - DefinitionToken.FromText("Hey"), - DefinitionToken.Close - })); - } - */ - } -} \ No newline at end of file diff --git a/Strict.Tokens.Tests/Strict.Tokens.Tests.csproj b/Strict.Tokens.Tests/Strict.Tokens.Tests.csproj deleted file mode 100644 index 15056572..00000000 --- a/Strict.Tokens.Tests/Strict.Tokens.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net5.0 - true - - false - - - - - - - - - - - - - diff --git a/Strict.Tokens.Tests/TokenTests.cs b/Strict.Tokens.Tests/TokenTests.cs deleted file mode 100644 index de73520c..00000000 --- a/Strict.Tokens.Tests/TokenTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -using NUnit.Framework; - -namespace Strict.Tokens.Tests -{ - public class TokenTests - { - [Test] - public void TokensWithTheSameTypeAreAlwaysTheSame() - { - Assert.That(DefinitionToken.Close, Is.EqualTo(DefinitionToken.Close)); - Assert.That(DefinitionToken.FromIdentifier("abc"), - Is.EqualTo(DefinitionToken.FromIdentifier("abc"))); - Assert.That(DefinitionToken.FromNumber("123"), - Is.EqualTo(DefinitionToken.FromNumber(123))); - } - - [Test] - public void TokenToString() - { - Assert.That(DefinitionToken.Open.ToString(), Is.EqualTo("(")); - Assert.That(DefinitionToken.FromNumber(123).ToString(), Is.EqualTo("123")); - Assert.That(DefinitionToken.FromIdentifier("Hello").ToString(), Is.EqualTo("Hello")); - } - /*unused - [Test] - public void KeywordTokenMustBeValid() => - Assert.Throws(() => - DefinitionToken.FromKeyword(nameof(KeywordTokenMustBeValid))); - - [Test] - public void OperatorTokenMustBeValid() => - Assert.Throws(() => - DefinitionToken.FromOperator(nameof(OperatorTokenMustBeValid))); - - [Test] - public void CheckIfIsBinaryOperator() - { - Assert.That(Operator.Open.IsBinaryOperator(), Is.False); - Assert.That(Operator.Plus.IsBinaryOperator(), Is.True); - } - */ - [Test] - public void CheckTokenType() - { - Assert.That(DefinitionToken.Open.IsIdentifier, Is.False); - Assert.That(DefinitionToken.Open.IsNumber, Is.False); - Assert.That(DefinitionToken.Close.IsText, Is.False); - Assert.That(DefinitionToken.FromIdentifier(nameof(CheckTokenType)).IsIdentifier, Is.True); - Assert.That(DefinitionToken.FromNumber(5).IsNumber, Is.True); - Assert.That(DefinitionToken.FromText(nameof(TokenTests)).IsText, Is.True); - } - } -} \ No newline at end of file diff --git a/Strict.Tokens/DefinitionToken.cs b/Strict.Tokens/DefinitionToken.cs deleted file mode 100644 index 3a90c0bf..00000000 --- a/Strict.Tokens/DefinitionToken.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text.RegularExpressions; - -namespace Strict.Tokens -{ - /// - /// Only for types up to method definitions, method bodies are parsed in the Expressions namespace - /// - public class DefinitionToken - { - /// - /// Very restricted, if a token exists already, we want to reuse the exact same one, this is true - /// for any number, keyword and even identifier, they all will return the same token if the value - /// or identifier name is the same. It makes parsing much easier as well without value checking. - /// - private DefinitionToken(string name, object? value = null) - { - Name = name; - Value = value; - } - - public string Name { get; } - public object? Value { get; } - public static bool IsValidNumber(string word) => double.TryParse(word, out _); - - public static DefinitionToken FromNumber(string word) => - FromNumber(Convert.ToDouble(word, CultureInfo.InvariantCulture)); - - public static DefinitionToken FromNumber(double value) - { - if (CachedNumbers.TryGetValue(value, out var existing)) - return existing; - var newNumber = new DefinitionToken(Number, value); - CachedNumbers.Add(value, newNumber); - return newNumber; - } - - public const string Number = nameof(Number); - private static readonly Dictionary CachedNumbers = new Dictionary(); - public bool IsNumber => Name == Number; - public const string From = "from"; - public const string Returns = "returns"; - - /// - /// Identifier words must have at least 3 characters, no numbers or special characters in them - /// and follow camelCase for private members and PascalCase for accessing public members or types. - /// - public static bool IsValidIdentifier(string word) => - word.Length >= 3 && (IsPrivateIdentifier(word) || IsPublicIdentifier(word)); - - private static bool IsPublicIdentifier(string word) => Regex.IsMatch(word, Public); - private static string Public => "^[A-Z][a-z]+(?:[A-Z][a-z]+)*$"; - private static bool IsPrivateIdentifier(string word) => Regex.IsMatch(word, Private); - private static string Private => "^[a-z][a-z]+(?:[A-Z][a-z]+)*$"; - public bool IsIdentifier => Name == nameof(Public) || Name == nameof(Private); - - public static DefinitionToken FromIdentifier(string name) - { - if (CachedIdentifiers.TryGetValue(name, out var existing)) - return existing; - var newIdentifier = new DefinitionToken(IsPublicIdentifier(name) - ? nameof(Public) - : nameof(Private), name); - CachedIdentifiers.Add(name, newIdentifier); - return newIdentifier; - } - - private static readonly Dictionary CachedIdentifiers = new Dictionary(); - - public override string ToString() => - Value != null - ? Value.ToString()! - : Name; - /*not longer needed - public static DefinitionToken FromKeyword(string keyword) => - keyword switch - { - Keyword.Test => Test, - Keyword.Is => Is, - Keyword.From => From, - Keyword.Let => Let, - Keyword.Return => Return, - Keyword.True => True, - Keyword.False => False, - _ => throw new InvalidKeyword(keyword) - }; - - public static DefinitionToken Test = new DefinitionToken(Keyword.Test); - public static DefinitionToken Is = new DefinitionToken(Keyword.Is); - public static DefinitionToken From = new DefinitionToken(Keyword.From); - public static DefinitionToken Let = new DefinitionToken(Keyword.Let); - public static DefinitionToken Return = new DefinitionToken(Keyword.Return); - public static DefinitionToken True = new DefinitionToken(Keyword.True, true); - public static DefinitionToken False = new DefinitionToken(Keyword.False, false); - public bool IsBoolean => Name == Keyword.True || Name == Keyword.False; - - public class InvalidKeyword : Exception - { - public InvalidKeyword(string keyword) : base(keyword) { } - } - - public static DefinitionToken FromOperator(string operatorSymbol) => - operatorSymbol switch - { - Operator.Plus => Plus, - Operator.Open => Open, - Operator.Close => Close, - Operator.Assign => Assign, - _ => throw new InvalidOperator(operatorSymbol) - }; - public static DefinitionToken Plus = new DefinitionToken(Operator.Plus); - public static DefinitionToken Assign = new DefinitionToken(Operator.Assign); - - public class InvalidOperator : Exception - { - public InvalidOperator(string operatorSymbol) : base(operatorSymbol) { } - } - public static DefinitionToken Dot = new DefinitionToken("."); - */ - public static DefinitionToken Open = new DefinitionToken("("); - public static DefinitionToken Close = new DefinitionToken(")"); - - public static DefinitionToken FromText(string name) - { - if (CachedTexts.TryGetValue(name, out var existing)) - return existing; - var newText = new DefinitionToken(Text, name); - CachedTexts.Add(name, newText); - return newText; - } - - public const string Text = nameof(Text); - private static readonly Dictionary CachedTexts = new Dictionary(); - public bool IsText => Name == Text; - } -} \ No newline at end of file diff --git a/Strict.Tokens/LineLexer.cs b/Strict.Tokens/LineLexer.cs deleted file mode 100644 index 5c72c89c..00000000 --- a/Strict.Tokens/LineLexer.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; - -namespace Strict.Tokens -{ - /// - /// Goes through each line and gives us basic Tokens for keywords and Identifiers. - /// See Expressions namespace for full parsing of method bodies. - /// - public class LineLexer - { - public LineLexer(Tokenizer tokens) => this.tokens = tokens; - private readonly Tokenizer tokens; - - public void Process(string line) - { - Tabs = 0; - previousCharacter = ' '; - for (Position = 0; Position < line.Length; Position++) - if (line[Position] == '\t') - Tabs++; - else if (Tabs == 0) - throw new LineMustStartWithTab(); - else - ParseToken(line[Position]); - if (word.Length > 0) - ParseWord(); - } - - public int Tabs { get; private set; } - public int Position { get; private set; } - public class LineMustStartWithTab : Exception { } - - private void ParseToken(in char character) - { - if (character == ' ' && previousCharacter != ')' || character == '(' || character == ')') - ParseWord(); - if (character == '(') - tokens.Add(DefinitionToken.Open); - else if (character == ')') - tokens.Add(DefinitionToken.Close); - else if (character != ' ' && previousCharacter != ')') - word += character; - previousCharacter = character; - } - - private void ParseWord() - { - if (string.IsNullOrEmpty(word)) - throw new UnexpectedSpaceOrEmptyParenthesisDetected(Position); - /*not used - if (word.IsKeyword()) - tokens.Add(DefinitionToken.FromKeyword(word)); - else if (word.IsOperator()) - tokens.Add(DefinitionToken.FromOperator(word)); - else */if (DefinitionToken.IsValidNumber(word)) - tokens.Add(DefinitionToken.FromNumber(word)); - else if (DefinitionToken.IsValidIdentifier(word)) - tokens.Add(DefinitionToken.FromIdentifier(word)); - else if (word.StartsWith('\"') && word.EndsWith('\"')) - tokens.Add(DefinitionToken.FromText(word[1..^1])); - /*also not used here - else if (word.Contains('.')) - AddIdentifierParts(); - */ - else - throw new InvalidIdentifierName(word, Position); - word = ""; - } - /*nah - private void AddIdentifierParts() - { - var split = word.Split('.'); - for (var index = 0; index < split.Length; index++) - { - if (index > 0) - tokens.Add(DefinitionToken.Dot); - tokens.Add(DefinitionToken.FromIdentifier(split[index])); - } - } - */ - private string word = ""; - private int previousCharacter; - - public class UnexpectedSpaceOrEmptyParenthesisDetected : Exception - { - public UnexpectedSpaceOrEmptyParenthesisDetected(in int position) : base( - "at position: " + position) { } - } - - public class InvalidIdentifierName : Exception - { - public InvalidIdentifierName(string word, int position) : base(word + " at position: " + - position) { } - } - } -} \ No newline at end of file diff --git a/Strict.Tokens/Strict.Tokens.csproj b/Strict.Tokens/Strict.Tokens.csproj deleted file mode 100644 index 5dbc0b2c..00000000 --- a/Strict.Tokens/Strict.Tokens.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - netcoreapp3.1 - latest - enable - true - - - - - true - - - - true - - - diff --git a/Strict.Tokens/Tokenizer.cs b/Strict.Tokens/Tokenizer.cs deleted file mode 100644 index ee37e20e..00000000 --- a/Strict.Tokens/Tokenizer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Strict.Tokens -{ - public interface Tokenizer - { - void Add(DefinitionToken token); - } -} \ No newline at end of file diff --git a/Strict.Validators.Tests/ConstantCollapserTests.cs b/Strict.Validators.Tests/ConstantCollapserTests.cs index 630ba449..3303b414 100644 --- a/Strict.Validators.Tests/ConstantCollapserTests.cs +++ b/Strict.Validators.Tests/ConstantCollapserTests.cs @@ -28,7 +28,6 @@ public void ComplainWhenAConstantIsUsedInANormalMember() new TypeLines(nameof(FoldMemberInitialValueExpressions), "has number = 17 + 4", "Run", "\tnumber")); simpleType.ParseMembersAndMethods(parser); - // ReSharper disable once AccessToDisposedClosure Assert.That(() => collapser.Visit(simpleType, true), Throws.InstanceOf()); } @@ -42,7 +41,7 @@ public void FoldTextToNumberToJustNumber() "\tfolded + 1" ]); collapser.Visit(method, true); - Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data, Is.EqualTo(6)); + Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(6)); } [Test] @@ -54,7 +53,33 @@ public void FoldNumberToTextToJustText() "\tfolded + \"yo\"" ]); collapser.Visit(method, true); - Assert.That(((Text)method.GetBodyAndParseIfNeeded()).Data, Is.EqualTo("5yo")); + Assert.That(((Text)method.GetBodyAndParseIfNeeded()).Data.Text, Is.EqualTo("5yo")); + } + + [Test] + public void FoldTwoConstants() + { + var method = new Method(type, 1, parser, [ + "Run", + "\tconstant hi = \"Hi\"", + "\thi + hi" + ]); + collapser.Visit(method, true); + Assert.That(((Text)method.GetBodyAndParseIfNeeded()).Data.Text, Is.EqualTo("HiHi")); + } + + [Test] + public void FoldTwoMembers() + { + using var foldType = new Type(TestPackage.Instance, + new TypeLines(nameof(FoldTwoMembers), + "has one = 1", + "has two = 2", + "Run Number", + "\tone + two")); + foldType.ParseMembersAndMethods(parser); + collapser.Visit(foldType.Methods[0], true); + Assert.That(((Number)foldType.Methods[0].GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(3)); } [Test] @@ -65,7 +90,7 @@ public void FoldBooleans() "\ttrue or false and true" ]); collapser.Visit(method, true); - Assert.That(((Boolean)method.GetBodyAndParseIfNeeded()).Data, Is.EqualTo(true)); + Assert.That(((Boolean)method.GetBodyAndParseIfNeeded()).Data.Boolean, Is.EqualTo(true)); } [Test] @@ -89,7 +114,7 @@ public void MultipleNestedConstantsGetFoldedToo() "\tsecond * 2" ]); collapser.Visit(method, true); - Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data, Is.EqualTo(10)); + Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(10)); } [Test] @@ -100,7 +125,7 @@ public void FoldMemberInitialValueExpressions() "constant number = 17 + 4", "Run", "\tnumber")); simpleType.ParseMembersAndMethods(parser); collapser.Visit(simpleType, true); - Assert.That(((Number)simpleType.Members[0].InitialValue!).Data, Is.EqualTo(21)); + Assert.That(((Number)simpleType.Members[0].InitialValue!).Data.Number, Is.EqualTo(21)); } [Test] @@ -112,8 +137,129 @@ public void FoldParameterDefaultValueExpressions() "\tnumber + number * 2")); simpleType.ParseMembersAndMethods(parser); collapser.Visit(simpleType, true); - Assert.That(((Number)simpleType.Methods[0].Parameters[1].DefaultValue!).Data, + Assert.That(((Number)simpleType.Methods[0].Parameters[1].DefaultValue!).Data.Number, Is.EqualTo(21)); - Assert.That(((Number)simpleType.Methods[^1].GetBodyAndParseIfNeeded()).Data, Is.EqualTo(3)); + Assert.That(((Number)simpleType.Methods[^1].GetBodyAndParseIfNeeded()).Data.Number, + Is.EqualTo(3)); + } + + [Test] + public void FoldNestedBinaryOnLeftSide() + { + var method = new Method(type, 1, parser, [ + "Run", + "\t(1 + 2) * 3" + ]); + collapser.Visit(method, true); + Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(9)); + } + + [Test] + public void FoldNestedBinaryOnRightSide() + { + var method = new Method(type, 1, parser, [ + "Run", + "\t1 * (2 + 3)" + ]); + collapser.Visit(method, true); + Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(5)); + } + + [Test] + public void FoldTextPlusNumber() + { + var method = new Method(type, 1, parser, [ + "Run", + "\t\"value\" + 5" + ]); + collapser.Visit(method, true); + Assert.That(((Text)method.GetBodyAndParseIfNeeded()).Data.Text, Is.EqualTo("value5")); + } + + [Test] + public void FoldMinusOperation() + { + var method = new Method(type, 1, parser, [ + "Run", + "\t9 - 4" + ]); + collapser.Visit(method, true); + Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(5)); + } + + [Test] + public void FoldDivideOperation() + { + var method = new Method(type, 1, parser, [ + "Run", + "\t9 / 2" + ]); + collapser.Visit(method, true); + Assert.That(((Number)method.GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(4.5)); + } + + [Test] + public void KeepExpressionWhenOperatorCannotBeCollapsed() + { + var method = new Method(type, 1, parser, [ + "Run", + "\ttrue xor false" + ]); + collapser.Visit(method, true); + Assert.That(method.GetBodyAndParseIfNeeded(), Is.InstanceOf()); + } + + [Test] + public void FoldTextPlusBoolean() + { + var method = new Method(type, 1, parser, [ + "Run", + "\t\"hello\" + true" + ]); + collapser.Visit(method, true); + Assert.That(((Text)method.GetBodyAndParseIfNeeded()).Data.Text, Is.EqualTo("helloTrue")); + } + + [Test] + public void FoldsMemberWithBinaryInitialValueOnLeftSide() + { + using var testType = new Type(TestPackage.Instance, + new TypeLines(nameof(FoldsMemberWithBinaryInitialValueOnLeftSide), + "constant one = 2 + 3", + "Run Number", + "\tone * 2")); + testType.ParseMembersAndMethods(parser); + collapser.Visit(testType.Methods[0], true); + Assert.That(((Number)testType.Methods[0].GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(10)); + } + + [Test] + public void FoldsMemberWithBinaryInitialValueOnRightSide() + { + using var testType = new Type(TestPackage.Instance, + new TypeLines(nameof(FoldsMemberWithBinaryInitialValueOnRightSide), + "constant one = 2 + 3", + "Run Number", + "\t2 * one")); + testType.ParseMembersAndMethods(parser); + collapser.Visit(testType.Methods[0], true); + Assert.That(((Number)testType.Methods[0].GetBodyAndParseIfNeeded()).Data.Number, Is.EqualTo(10)); + } + + [Test] + public void SubstitutesConstantMemberCallInBinaryWhenOtherSideIsNonConstant() + { + using var testType = new Type(TestPackage.Instance, + new TypeLines("PartialBinaryCollapse", + "constant one = 1", + "has number", + "AddConstant Number", + "\tone + number", + "Run", + "\tnumber")); + testType.ParseMembersAndMethods(parser); + collapser.Visit(testType.Methods[0], true); + Assert.That(((Binary)testType.Methods[0].GetBodyAndParseIfNeeded()).Instance, + Is.InstanceOf()); } } \ No newline at end of file diff --git a/Strict.Validators.Tests/Strict.Validators.Tests.csproj b/Strict.Validators.Tests/Strict.Validators.Tests.csproj index bfb0c70a..2c22d37b 100644 --- a/Strict.Validators.Tests/Strict.Validators.Tests.csproj +++ b/Strict.Validators.Tests/Strict.Validators.Tests.csproj @@ -11,17 +11,10 @@ + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Strict.Validators.Tests/TypeValidatorTests.cs b/Strict.Validators.Tests/TypeValidatorTests.cs index 0a87e2f2..3115e246 100644 --- a/Strict.Validators.Tests/TypeValidatorTests.cs +++ b/Strict.Validators.Tests/TypeValidatorTests.cs @@ -152,7 +152,7 @@ public void ValidateTypeHasTooManyDependenciesFromMethod() => "\t0" ])), Throws.InstanceOf().With.Message.Contains( - "Type TestPackage.ValidateTypeHasTooManyDependenciesFromMethod from constructor method " + + "Type TestPackage/ValidateTypeHasTooManyDependenciesFromMethod from constructor method " + "has parameters count 5 but limit is 4")); [Test] diff --git a/Strict.Validators/ConstantCollapser.cs b/Strict.Validators/ConstantCollapser.cs index 0a36049a..5ee22891 100644 --- a/Strict.Validators/ConstantCollapser.cs +++ b/Strict.Validators/ConstantCollapser.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using Boolean = Strict.Expressions.Boolean; using Type = Strict.Language.Type; @@ -62,28 +62,27 @@ protected override void Visit(Body body, object? context = null) left = leftMember.Member.InitialValue; var right = binary.Arguments[0]; if (right is VariableCall { Variable.InitialValue.IsConstant: true } rightCall) - right = rightCall.Variable.InitialValue; //ncrunch: no coverage + right = rightCall.Variable.InitialValue; if (right is MemberCall { Member.InitialValue.IsConstant: true } rightMember) - right = rightMember.Member.InitialValue; //ncrunch: no coverage + right = rightMember.Member.InitialValue; var collapsedExpression = TryCollapseBinaryExpression(left, right, binary.Method); if (collapsedExpression != null) return collapsedExpression; - //ncrunch: no coverage start if (!ReferenceEquals(left, binary.Instance!) || !ReferenceEquals(right, binary.Arguments[0])) { var arguments = new[] { right }; return new Binary(left, left.ReturnType.GetMethod(binary.Method.Name, arguments), arguments); } - } //ncrunch: no coverage end + } if (!expression.IsConstant) - return expression; //ncrunch: no coverage + return expression; if (expression is To to) { var value = to.Instance as Value; - if (to.ConversionType.Name == Base.Number && value?.Data is string text) - return new Number(to.Method.Type, double.Parse(text)); - if (to.ConversionType.Name == Base.Text && value?.Data is double number) - return new Text(to.Method.Type, number.ToString(CultureInfo.InvariantCulture)); + if (to.ConversionType.IsNumber && value is Text textValue) + return new Number(to.Method.Type, double.Parse(textValue.Data.Text)); + if (to.ConversionType.IsText && value is Number numberValue) + return new Text(to.Method.Type, numberValue.Data.Number.ToString(CultureInfo.InvariantCulture)); throw new UnsupportedToExpression(to.ToStringWithType()); //ncrunch: no coverage } return expression; @@ -91,49 +90,43 @@ protected override void Visit(Body body, object? context = null) public class UnsupportedToExpression(string toStringWithType) : Exception(toStringWithType); //ncrunch: no coverage - /// - /// Would be nice if all of these are evaluated via actual strict code! - /// private static Expression? TryCollapseBinaryExpression(Expression left, Expression right, Context method) { if (left is Binary leftBinary) - left = TryCollapseBinaryExpression(leftBinary.Instance!, leftBinary.Arguments[0], leftBinary.Method) ?? left; //ncrunch: no coverage + left = TryCollapseBinaryExpression(leftBinary.Instance!, leftBinary.Arguments[0], + leftBinary.Method) ?? left; if (right is Binary rightBinary) - right = TryCollapseBinaryExpression(rightBinary.Instance!, rightBinary.Arguments[0], rightBinary.Method) ?? right; //ncrunch: no coverage + right = TryCollapseBinaryExpression(rightBinary.Instance!, rightBinary.Arguments[0], + rightBinary.Method) ?? right; var leftNumber = left as Number; var rightNumber = right as Number; if (method.Name == BinaryOperator.Plus) { if (leftNumber != null && rightNumber != null) - return new Number(method, (double)leftNumber.Data + (double)rightNumber.Data); + return new Number(method, leftNumber.Data.Number + rightNumber.Data.Number); var leftText = left as Text; var rightText = right as Text; if (leftText != null && rightText != null) - return new Text(method, (string)leftText.Data + (string)rightText.Data); - //ncrunch: no coverage start + return new Text(method, leftText.Data.Text + rightText.Data.Text); if (leftText != null && rightNumber != null) - return new Text(method, (string)leftText.Data + rightNumber.Data); - if (leftNumber != null && rightText != null) - return new Text(method, (double)leftNumber.Data + (string)rightText.Data); + return new Text(method, leftText.Data.Text + rightNumber.Data.ToExpressionCodeString()); if (leftText != null && right is Boolean rightBool) - return new Text(method, (string)leftText.Data + rightBool.Data); - if (left is Boolean leftBool && rightText != null) - return new Text(method, leftBool.Data + (string)rightText.Data); + return new Text(method, leftText.Data.Text + rightBool.Data.Boolean); } else if (method.Name == BinaryOperator.Minus && leftNumber != null && rightNumber != null) - return new Number(method, (double)leftNumber.Data - (double)rightNumber.Data); + return new Number(method, leftNumber.Data.Number - rightNumber.Data.Number); else if (method.Name == BinaryOperator.Multiply && leftNumber != null && rightNumber != null) - return new Number(method, (double)leftNumber.Data * (double)rightNumber.Data); + return new Number(method, leftNumber.Data.Number * rightNumber.Data.Number); else if (method.Name == BinaryOperator.Divide && leftNumber != null && rightNumber != null) - return new Number(method, (double)leftNumber.Data / (double)rightNumber.Data); + return new Number(method, leftNumber.Data.Number / rightNumber.Data.Number); if (left is Boolean leftBoolean && right is Boolean rightBoolean) { if (method.Name == BinaryOperator.And) - return new Boolean(method, (bool)leftBoolean.Data && (bool)rightBoolean.Data); + return new Boolean(method, leftBoolean.Data.Boolean && rightBoolean.Data.Boolean); if (method.Name == BinaryOperator.Or) - return new Boolean(method, (bool)leftBoolean.Data || (bool)rightBoolean.Data); + return new Boolean(method, leftBoolean.Data.Boolean || rightBoolean.Data.Boolean); } - return null; //ncrunch: no coverage end + return null; } } \ No newline at end of file diff --git a/Strict.Validators/Visitor.cs b/Strict.Validators/Visitor.cs index 817ce41c..d9c39d69 100644 --- a/Strict.Validators/Visitor.cs +++ b/Strict.Validators/Visitor.cs @@ -14,13 +14,13 @@ public abstract class Visitor { public void Visit(Package package, object? context = null) { - foreach (var type in package) - Visit(type, context); + foreach (var type in package.Types) + Visit(type.Value, context); } public virtual void Visit(Type type, object? context = null) { - if (type.Name == Base.Any) + if (type.IsAny) return; foreach (var member in type.Members) Visit(member, context); @@ -88,7 +88,7 @@ protected virtual void Visit(Body body, object? context = null) return expression; if (expression is Body innerBody) Visit(innerBody, context); //ncrunch: no coverage - if (expression is Binary binary) + else if (expression is Binary binary) { var changedInstance = Visit(binary.Instance, body, context)!; var rewrittenArgument = Visit(binary.Arguments[0], body, context)!; @@ -96,7 +96,7 @@ protected virtual void Visit(Body body, object? context = null) !ReferenceEquals(rewrittenArgument, binary.Arguments[0])) return new Binary(changedInstance, binary.Method, [rewrittenArgument]); } - if (expression is Declaration declaration) + else if (expression is Declaration declaration) { var newValue = Visit(declaration.Value, body, context)!; if (!ReferenceEquals(newValue, declaration.Value) && body != null) diff --git a/Strict.VirtualMachine.Tests/BytecodeInterpreterTests.cs b/Strict.VirtualMachine.Tests/BytecodeInterpreterTests.cs deleted file mode 100644 index 40c8f44d..00000000 --- a/Strict.VirtualMachine.Tests/BytecodeInterpreterTests.cs +++ /dev/null @@ -1,461 +0,0 @@ -namespace Strict.Runtime.Tests; - -public class BytecodeInterpreterTests : BaseVirtualMachineTests -{ - [SetUp] - public void Setup() => vm = new BytecodeInterpreter(); - - protected BytecodeInterpreter vm = null!; - - private void CreateSampleEnum() - { - if (type.Package.FindDirectType("Days") == null) - new Type(type.Package, - new TypeLines("Days", "constant Monday = 1", "constant Tuesday = 2", - "constant Wednesday = 3", "constant Friday = 5")). - ParseMembersAndMethods(new MethodExpressionParser()); - } - - [Test] - public void ReturnEnum() - { - CreateSampleEnum(); - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(ReturnEnum), - nameof(ReturnEnum) + "(5).GetMonday", "has dummy Number", "GetMonday Number", - "\tDays.Monday")).Generate(); - var result = vm.Execute(statements).Returns; - Assert.That(result!.Value, Is.EqualTo(1)); - } - - [Test] - public void EnumIfConditionComparison() - { - CreateSampleEnum(); - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(EnumIfConditionComparison), - nameof(EnumIfConditionComparison) + "(5).GetMonday(Days.Monday)", "has dummy Number", - "GetMonday(days) Boolean", - "\tif days is Days.Monday", "\t\treturn true", "\telse", "\t\treturn false")).Generate(); - var result = vm.Execute(statements).Returns; - Assert.That(result!.Value, Is.EqualTo(true)); - } - - [TestCase(Instruction.Add, 15, 5, 10)] - [TestCase(Instruction.Subtract, 5, 8, 3)] - [TestCase(Instruction.Multiply, 4, 2, 2)] - [TestCase(Instruction.Divide, 3, 7.5, 2.5)] - [TestCase(Instruction.Modulo, 1, 5, 2)] - [TestCase(Instruction.Add, "510", "5", 10)] - [TestCase(Instruction.Add, "510", 5, "10")] - [TestCase(Instruction.Add, "510", "5", "10")] - public void Execute(Instruction operation, object expected, params object[] inputs) => - Assert.That(vm.Execute(BuildStatements(inputs, operation)).Memory.Registers[Register.R1].Value, - Is.EqualTo(expected)); - - private static Statement[] - BuildStatements(IReadOnlyList inputs, Instruction operation) => - [ - new SetStatement(new Instance(inputs[0] is int - ? NumberType - : TextType, inputs[0]), Register.R0), - new SetStatement(new Instance(inputs[1] is int - ? NumberType - : TextType, inputs[1]), Register.R1), - new Binary(operation, Register.R0, Register.R1) - ]; - - [Test] - public void LoadVariable() => - Assert.That( - vm.Execute([ - new LoadConstantStatement(Register.R0, new Instance(NumberType, 5)) - ]).Memory.Registers[Register.R0].Value, Is.EqualTo(5)); - - [Test] - public void SetAndAdd() => - Assert.That( - vm.Execute([ - new LoadConstantStatement(Register.R0, new Instance(NumberType, 10)), - new LoadConstantStatement(Register.R1, new Instance(NumberType, 5)), - new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2) - ]).Memory.Registers[Register.R2].Value, Is.EqualTo(15)); - - [Test] - public void AddFiveTimes() => - Assert.That(vm.Execute([ - new SetStatement(new Instance(NumberType, 5), Register.R0), - new SetStatement(new Instance(NumberType, 1), Register.R1), - new SetStatement(new Instance(NumberType, 0), Register.R2), - new Binary(Instruction.Add, Register.R0, Register.R2, Register.R2), - new Binary(Instruction.Subtract, Register.R0, Register.R1, Register.R0), - new JumpIfNotZero(-3, Register.R0) - ]).Memory.Registers[Register.R2].Value, Is.EqualTo(0 + 5 + 4 + 3 + 2 + 1)); - - [TestCase("ArithmeticFunction(10, 5).Calculate(\"add\")", 15)] - [TestCase("ArithmeticFunction(10, 5).Calculate(\"subtract\")", 5)] - [TestCase("ArithmeticFunction(10, 5).Calculate(\"multiply\")", 50)] - [TestCase("ArithmeticFunction(10, 5).Calculate(\"divide\")", 2)] - public void RunArithmeticFunctionExample(string methodCall, int expectedResult) - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource("ArithmeticFunction", - methodCall, ArithmeticFunctionExample)).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(expectedResult)); - } - - [Test] - public void AccessListByIndex() - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(AccessListByIndex), - nameof(AccessListByIndex) + "(1, 2, 3, 4, 5).Get(2)", - "has numbers", - "Get(index Number) Number", - "\tnumbers(index)")).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(3)); - } - - [Test] - public void AccessListByIndexNonNumberType() - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource( - nameof(AccessListByIndexNonNumberType), - nameof(AccessListByIndexNonNumberType) + "(\"1\", \"2\", \"3\", \"4\", \"5\").Get(2)", - "has texts", "Get(index Number) Text", - "\ttexts(index)")).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo("3")); - } - - [Test] - public void ReduceButGrowLoopExample() => - Assert.That( - vm.Execute([ - new StoreVariableStatement(new Instance(NumberType, 10), "number"), - new StoreVariableStatement(new Instance(NumberType, 1), "result"), - new StoreVariableStatement(new Instance(NumberType, 2), "multiplier"), - new LoadVariableToRegister(Register.R0, "number"), - new LoopBeginStatement(Register.R0), new LoadVariableToRegister(Register.R2, "result"), - new LoadVariableToRegister(Register.R3, "multiplier"), - new Binary(Instruction.Multiply, Register.R2, Register.R3, Register.R4), - new StoreFromRegisterStatement(Register.R4, "result"), - new LoopEndStatement(5), - new LoadVariableToRegister(Register.R5, "result"), new Return(Register.R5) - ]).Returns?.Value, Is.EqualTo(1024)); - - [TestCase("NumberConvertor", "NumberConvertor(5).ConvertToText", "5", - "has number", - "ConvertToText Text", - "\t5 to Text")] - [TestCase("TextConvertor", "TextConvertor(\"5\").ConvertToNumber", 5, - "has text", - "ConvertToNumber Number", - "\ttext to Number")] - public void ExecuteToOperator(string programName, string methodCall, object expected, params string[] code) - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(programName, - methodCall, code)).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(expected)); - } - - //ncrunch: no coverage start - private static IEnumerable MethodCallTests - { - get - { - yield return new TestCaseData("AddNumbers", "AddNumbers(2, 5).GetSum", SimpleMethodCallCode, 7); - yield return new TestCaseData("CallWithConstants", "CallWithConstants(2, 5).GetSum", MethodCallWithConstantValues, 6); - yield return new TestCaseData("CallWithoutArguments", "CallWithoutArguments(2, 5).GetSum", MethodCallWithLocalWithNoArguments, 542); - yield return new TestCaseData("CurrentlyFailing", "CurrentlyFailing(10).SumEvenNumbers", CurrentlyFailingTest, 20); - } - } //ncrunch: no coverage end - - [TestCaseSource(nameof(MethodCallTests))] - // ReSharper disable TooManyArguments, makes below tests easier - public void MethodCall(string programName, string methodCall, string[] source, object expected) - { - var statements = - new ByteCodeGenerator(GenerateMethodCallFromSource(programName, methodCall, - source)).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(expected)); - } - - [Test] - public void IfAndElseTest() - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource("IfAndElseTest", - "IfAndElseTest(3).IsEven", IfAndElseTestCode)).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, - Is.EqualTo("Number is less or equal than 10")); - } - - [TestCase("EvenSumCalculator(100).IsEven", 2450, "EvenSumCalculator", - new[] - { - "has number", - "IsEven Number", - "\tmutable sum = 0", - "\tfor number", - "\t\tif index % 2 is 0", - "\t\t\tsum = sum + index", - "\tsum" - })] - [TestCase("EvenSumCalculatorForList(100, 200, 300).IsEvenList", 2, "EvenSumCalculatorForList", - new[] - { - "has numbers", - "IsEvenList Number", - "\tmutable sum = 0", - "\tfor numbers", - "\t\tif index % 2 is 0", - "\t\t\tsum = sum + index", - "\tsum" - })] - public void CompileCompositeBinariesInIfCorrectlyWithModulo(string methodCall, - object expectedResult, string methodName, params string[] code) - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(methodName, - methodCall, code)).Generate(); - Assert.That(vm.Execute(statements).Returns?.Value, Is.EqualTo(expectedResult)); - } - - [TestCase("AddToTheList(5).Add", "100 200 300 400 0 1 2 3", "AddToTheList", - new[] - { - "has number", - "Add Numbers", - "\tmutable myList = (100, 200, 300, 400)", - "\tfor myList", - "\t\tif value % 2 is 0", - "\t\t\tmyList = myList + index", - "\tmyList" - })] - [TestCase("RemoveFromTheList(5).Remove", "100 200 300", "RemoveFromTheList", - new[] - { - "has number", - "Remove Numbers", - "\tmutable myList = (100, 200, 300, 400)", - "\tfor myList", - "\t\tif value is 400", - "\t\t\tmyList = myList - 400", - "\tmyList" - })] - [TestCase("RemoveB(\"s\", \"b\", \"s\").Remove", "s s", "RemoveB", - new[] - { - "has texts", - "Remove Texts", - "\tmutable textList = texts", - "\tfor texts", - "\t\tif value is \"b\"", - "\t\t\ttextList = textList - value", - "\ttextList" - })] - [TestCase("ListRemove(\"s\", \"b\", \"s\").Remove", "s s", "ListRemove", - new[] - { - "has texts", - "Remove Texts", - "\tmutable textList = texts", - "\ttextList.Remove(\"b\")", - "\ttextList" - })] - [TestCase("ListRemoveMultiple(\"s\", \"b\", \"s\").Remove", "b", "ListRemoveMultiple", - new[] - { - "has texts", - "Remove Texts", - "\tmutable textList = texts", - "\ttextList.Remove(\"s\")", - "\ttextList" - })] - public void ExecuteListBinaryOperations(string methodCall, - object expectedResult, string programName, params string[] code) - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(programName, - methodCall, code)).Generate(); - var values = (List)vm.Execute(statements).Returns?.Value!; - var elements = values.Aggregate("", (current, value) => current + ((Value)value).Data + " "); - Assert.That(elements.Trim(), Is.EqualTo(expectedResult)); - } //ncrunch: no coverage end - - [TestCase("TestContains(\"s\", \"b\", \"s\").Contains(\"b\")", "True", "TestContains", - new[] - { - "has elements Texts", - "Contains(other Text) Boolean", - "\tfor elements", - "\t\tif value is other", - "\t\t\treturn true", - "\tfalse" - })] - public void CallCommonMethodCalls(string methodCall, object expectedResult, - string programName, params string[] code) - { - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(programName, - methodCall, code)).Generate(); - var result = vm.Execute(statements).Returns?.Value!; - Assert.That(result.ToString(), Is.EqualTo(expectedResult)); - } - - [TestCase("CollectionAdd(5).AddNumberToList", - "1 2 3 5", - "has number", - "AddNumberToList Numbers", - "\tmutable numbers = (1, 2, 3)", - "\tnumbers.Add(number)", - "\tnumbers")] - public void CollectionAdd(string methodCall, string expected, params string[] code) - { - var statements = - new ByteCodeGenerator( - GenerateMethodCallFromSource(nameof(CollectionAdd), methodCall, code)).Generate(); - var result = ExpressionListToSpaceSeparatedString(statements); - Assert.That(result.TrimEnd(), Is.EqualTo(expected)); - } - - private string ExpressionListToSpaceSeparatedString(IList statements) => - ((IEnumerable)vm.Execute(statements).Returns?.Value!).Aggregate("", - (current, value) => current + ((Value)value).Data + " "); - - [Test] - public void DictionaryAdd() - { - string[] code = - [ - "has number", - "RemoveFromDictionary Number", - "\tmutable values = Dictionary(Number, Number)", "\tvalues.Add(1, number)", "\tnumber" - ]; - Assert.That( - ((Dictionary)vm. - Execute(new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(DictionaryAdd), - "DictionaryAdd(5).RemoveFromDictionary", code)).Generate()).Memory.Variables["values"]. - Value).Count, Is.EqualTo(1)); - } - - [Test] - public void CreateEmptyDictionaryFromConstructor() - { - var dictionaryType = TestPackage.Instance.GetType(Base.Dictionary). - GetGenericImplementation(NumberType, NumberType); - var methodCall = CreateFromMethodCall(dictionaryType); - var statements = new List { new Invoke(Register.R0, methodCall, new Registry()) }; - var result = vm.Execute(statements).Memory.Registers[Register.R0].Value; - Assert.That(result, Is.InstanceOf>().And.Count.EqualTo(0)); - } - - [TestCase("DictionaryGet(5).AddToDictionary", - "5", - "has number", - "AddToDictionary Number", - "\tmutable values = Dictionary(Number, Number)", - "\tvalues.Add(1, number)", - "\tvalues.Get(1)")] - public void DictionaryGet(string methodCall, string expected, params string[] code) - { - var statements = - new ByteCodeGenerator( - GenerateMethodCallFromSource(nameof(DictionaryGet), methodCall, code)).Generate(); - var result = vm.Execute(statements).Returns?.Value!; - Assert.That(result.ToString(), Is.EqualTo(expected)); - } - - [TestCase("DictionaryRemove(5).AddToDictionary", - "5", - "has number", - "AddToDictionary Number", - "\tmutable values = Dictionary(Number, Number)", - "\tvalues.Add(1, number)", - "\tvalues.Add(2, number + 10)", - "\tvalues.Get(2)")] - public void DictionaryRemove(string methodCall, string expected, params string[] code) - { - var statements = - new ByteCodeGenerator( - GenerateMethodCallFromSource(nameof(DictionaryRemove), methodCall, code)).Generate(); - var result = vm.Execute(statements).Returns?.Value!; - Assert.That(result.ToString(), Is.EqualTo("15")); - } - - [Test] - public void ReturnWithinALoop() - { - var source = new[] - { - "has number", "GetAll Number", "\tfor number", "\t\tvalue" - }; - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(ReturnWithinALoop), - "ReturnWithinALoop(5).GetAll", source)).Generate(); - Assert.That(() => vm.Execute(statements).Returns?.Value, Is.EqualTo(1 + 2 + 3 + 4 + 5)); - } - - [Test] - public void ReverseWithRange() - { - var source = new[] - { - "has numbers", "Reverse Numbers", "\tmutable result = Numbers", "\tlet len = numbers.Length - 1", "\tfor Range(len, 0)", - "\t\tresult.Add(numbers(index))", "\tresult" - }; - var statements = new ByteCodeGenerator(GenerateMethodCallFromSource(nameof(ReverseWithRange), - "ReverseWithRange(1, 2, 3).Reverse", source)).Generate(); - Assert.That(() => ExpressionListToSpaceSeparatedString(statements), Is.EqualTo("3 2 1 ")); - } - - [Test] - public void ConditionalJump() => - Assert.That( - vm.Execute([ - new SetStatement(new Instance(NumberType, 5), Register.R0), - new SetStatement(new Instance(NumberType, 1), Register.R1), - new SetStatement(new Instance(NumberType, 10), Register.R2), - new Binary(Instruction.LessThan, Register.R2, Register.R0), - new JumpIf(Instruction.JumpIfTrue, 2), - new Binary(Instruction.Add, Register.R2, Register.R0, Register.R0) - ]).Memory.Registers[Register.R0].Value, Is.EqualTo(15)); - - [Test] - public void JumpIfTrueSkipsNextInstruction() => - Assert.That( - vm.Execute([ - new SetStatement(new Instance(NumberType, 1), Register.R0), - new SetStatement(new Instance(NumberType, 1), Register.R1), - new SetStatement(new Instance(NumberType, 0), Register.R2), - new Binary(Instruction.Equal, Register.R0, Register.R1), - new JumpIfTrue(1, Register.R0), - new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2) - ]).Memory.Registers[Register.R2].Value, Is.EqualTo(0)); - - [Test] - public void JumpIfFalseSkipsNextInstruction() => - Assert.That( - vm.Execute([ - new SetStatement(new Instance(NumberType, 1), Register.R0), - new SetStatement(new Instance(NumberType, 2), Register.R1), - new SetStatement(new Instance(NumberType, 0), Register.R2), - new Binary(Instruction.Equal, Register.R0, Register.R1), - new JumpIfFalse(1, Register.R0), - new Binary(Instruction.Add, Register.R0, Register.R1, Register.R2) - ]).Memory.Registers[Register.R2].Value, Is.EqualTo(0)); - - [TestCase(Instruction.GreaterThan, new[] { 1, 2 }, 2 - 1)] - [TestCase(Instruction.LessThan, new[] { 1, 2 }, 1 + 2)] - [TestCase(Instruction.Equal, new[] { 5, 5 }, 5 + 5)] - [TestCase(Instruction.NotEqual, new[] { 5, 5 }, 5 - 5)] - public void ConditionalJumpIfAndElse(Instruction conditional, int[] registers, int expected) => - Assert.That( - vm.Execute([ - new SetStatement(new Instance(NumberType, registers[0]), Register.R0), - new SetStatement(new Instance(NumberType, registers[1]), Register.R1), - new Binary(conditional, Register.R0, Register.R1), - new JumpIf(Instruction.JumpIfTrue, 2), - new Binary(Instruction.Subtract, Register.R1, Register.R0, Register.R0), - new JumpIf(Instruction.JumpIfFalse, 2), - new Binary(Instruction.Add, Register.R0, Register.R1, Register.R0) - ]).Memory.Registers[Register.R0].Value, Is.EqualTo(expected)); - - [TestCase(Instruction.Add)] - [TestCase(Instruction.GreaterThan)] - public void OperandsRequired(Instruction instruction) => - Assert.That( - () => vm.Execute([new Binary(instruction, Register.R0)]), - Throws.InstanceOf()); -} \ No newline at end of file diff --git a/Strict.VirtualMachine.Tests/InstanceTests.cs b/Strict.VirtualMachine.Tests/InstanceTests.cs deleted file mode 100644 index e54824a3..00000000 --- a/Strict.VirtualMachine.Tests/InstanceTests.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Strict.Runtime.Tests; - -public sealed class InstanceTests : BaseVirtualMachineTests -{ - [Test] - public void ListSubtractionRemovesExpressionInstance() - { - var targetExpression = new Value(NumberType, 5); - var left = new Instance(ListType.GetGenericImplementation(NumberType), - new List { targetExpression, new Value(NumberType, 7) }); - var right = new Instance(Base.Number, targetExpression); - var result = left - right; - var resultElements = (List)result.Value; - Assert.That(resultElements, Has.Count.EqualTo(1)); - Assert.That(((Value)resultElements[0]).Data, Is.EqualTo(7)); - } -} \ No newline at end of file diff --git a/Strict.VirtualMachine/Instance.cs b/Strict.VirtualMachine/Instance.cs deleted file mode 100644 index f61ea03a..00000000 --- a/Strict.VirtualMachine/Instance.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Strict.Expressions; -using Strict.Language; -using Type = Strict.Language.Type; - -namespace Strict.Runtime; - -/// -/// The only place where we can have a "static" method call to one of the from methods of a type -/// before we have a type instance yet, it is the only way to create instances. -/// -public sealed class Instance -{ - public Instance(Type? type, object value) - { - ReturnType = type; - if (value is Value valueObj) - Value = valueObj.Data; - else - Value = value; - } - - public Instance(string typeName, object value) - { - Value = value; - TypeName = typeName; - } - - public Instance(Expression expression, bool isMember = false) - { - ReturnType = expression.ReturnType; - if (expression is Value value) - Value = value.Data; - else - Value = expression; - IsMember = isMember; - } - - public bool IsMember { get; } - public Type? ReturnType { get; } - public string TypeName { get => ReturnType?.Name ?? field; } = string.Empty; - public object Value { get; set; } - - public object GetRawValue() - { - if (Value is Value value) - return value.Data; - return Value; - } - - public static Instance operator +(Instance left, Instance right) - { - if (!left.TypeName.StartsWith(Base.List, StringComparison.Ordinal)) - return HandleTextTypeConversionForBinaryOperations(left, right, BinaryOperator.Plus); - return left.ReturnType is GenericTypeImplementation { Name: Base.List } - ? new Instance(left.ReturnType, left.Value + right.Value.ToString()) - : AddElementToTheListAndGetInstance(left, right); - } - - private static Instance HandleTextTypeConversionForBinaryOperations(Instance left, - Instance right, string binaryOperator) - { - var leftReturnTypeName = left.TypeName; - var rightReturnTypeName = right.TypeName; - if (leftReturnTypeName == Base.Number && rightReturnTypeName == Base.Number) - return new Instance(right.ReturnType ?? left.ReturnType, - binaryOperator == BinaryOperator.Plus - ? Convert.ToDouble(left.Value) + Convert.ToDouble(right.Value) - : Convert.ToDouble(left.Value) - Convert.ToDouble(right.Value)); - if (leftReturnTypeName == Base.Text && rightReturnTypeName == Base.Text) - return new Instance(right.ReturnType ?? left.ReturnType, - left.Value.ToString() + right.Value); - if (rightReturnTypeName == Base.Text && leftReturnTypeName == Base.Number) - return new Instance(right.ReturnType, left.Value.ToString() + right.Value); - return new Instance(left.ReturnType, left.Value + right.Value.ToString()); - } - - public static Instance operator -(Instance left, Instance right) - { - if (!left.TypeName.StartsWith("List", StringComparison.Ordinal)) - return new Instance(left.ReturnType, - Convert.ToDouble(left.Value) - Convert.ToDouble(right.Value)); - var elements = new List((List)left.Value); - if (right.Value is Expression rightExpression) - elements.Remove(rightExpression); - else - elements.RemoveAt(elements.FindIndex(element => ((Value)element).Data.Equals(right.Value))); - return new Instance(left.ReturnType, elements); - } - - public static bool operator >(Instance left, Instance right) - { - return Convert.ToDouble(left.Value) > Convert.ToDouble(right.Value); - } - - public static bool operator <(Instance left, Instance right) - { - return Convert.ToDouble(left.Value) < Convert.ToDouble(right.Value); - } - - private static Instance AddElementToTheListAndGetInstance(Instance left, Instance right) - { - var elements = new List((List)left.Value); - var rightValue = new Value(elements.First().ReturnType, right.Value); - elements.Add(rightValue); - return new Instance(left.ReturnType, elements); - } - - public override string ToString() => $"{Value} {TypeName}"; -} \ No newline at end of file diff --git a/Strict.VirtualMachine/Memory.cs b/Strict.VirtualMachine/Memory.cs deleted file mode 100644 index ea55ea7d..00000000 --- a/Strict.VirtualMachine/Memory.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Strict.Expressions; -using Strict.Language; - -namespace Strict.Runtime; - -public sealed class Memory -{ - public void AddToCollectionVariable(string key, object element) - { - Variables.TryGetValue(key, out var collection); - if (collection?.Value is List listExpression) - listExpression.Add(listExpression.Count > 0 - ? ConvertObjectToValueForm(element, listExpression[0]) - : ConvertToValueFormWhenListIsEmpty(collection, element)); - } - - public Dictionary Variables = new(); - public Dictionary Registers { get; init; } = new(); - - public void AddToDictionary(string variableKey, Instance keyToAddTo, Instance value) - { - Variables.TryGetValue(variableKey, out var collection); - if (collection?.Value is Dictionary dictionary && - keyToAddTo.ReturnType != null && value.ReturnType != null) - dictionary.Add(new Value(keyToAddTo.ReturnType, keyToAddTo.Value), - new Value(value.ReturnType, value.Value)); - } - - private static Value ConvertObjectToValueForm(object obj, Expression prototype) => - new(prototype.ReturnType, obj); - - private static Value ConvertToValueFormWhenListIsEmpty(Instance collection, object element) => - collection.ReturnType is not GenericTypeImplementation genericImplementationType - ? throw new InvalidOperationException() - : new Value(genericImplementationType.ImplementationTypes[0], element); -} \ No newline at end of file diff --git a/Strict.VirtualMachine/Statements/InstanceStatement.cs b/Strict.VirtualMachine/Statements/InstanceStatement.cs deleted file mode 100644 index 53f6a99a..00000000 --- a/Strict.VirtualMachine/Statements/InstanceStatement.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Strict.Runtime.Statements; - -public abstract class InstanceStatement(Instruction instruction, Instance instance) : Statement(instruction) -{ - public Instance Instance { get; } = instance; - public override string ToString() => $"{Instruction} {Instance.Value}"; -} \ No newline at end of file diff --git a/Strict.VirtualMachine/Statements/LoopBeginStatement.cs b/Strict.VirtualMachine/Statements/LoopBeginStatement.cs deleted file mode 100644 index ccbdf6c0..00000000 --- a/Strict.VirtualMachine/Statements/LoopBeginStatement.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Strict.Runtime.Statements; - -public sealed class LoopBeginStatement : RegisterStatement -{ - public LoopBeginStatement(Register register) : base(Instruction.LoopBegin, register) { } - - /// Range loop: from startIndex to endIndex register (inclusive). - public LoopBeginStatement(Register startIndex, Register endIndex) - : base(Instruction.LoopBegin, startIndex) - { - EndIndex = endIndex; - IsRange = true; - } - - public Register? EndIndex { get; } - public bool IsRange { get; } - /// Loop execution state - set during Execute, reset before each new run. - public bool IsInitialized { get; set; } - public int LoopCount { get; set; } - public int? StartIndexValue { get; private set; } - public int? EndIndexValue { get; private set; } - public bool? IsDecreasing { get; private set; } - - public void InitializeRangeState(int startIndex, int endIndex) - { - StartIndexValue = startIndex; - EndIndexValue = endIndex; - IsDecreasing = endIndex < startIndex; - LoopCount = (IsDecreasing.Value - ? startIndex - endIndex - : endIndex - startIndex) + 1; - IsInitialized = true; - } - - public void Reset() - { - IsInitialized = false; - LoopCount = 0; - StartIndexValue = null; - EndIndexValue = null; - IsDecreasing = null; - } -} diff --git a/Strict.VirtualMachine/Statements/SetStatement.cs b/Strict.VirtualMachine/Statements/SetStatement.cs deleted file mode 100644 index d21bcd33..00000000 --- a/Strict.VirtualMachine/Statements/SetStatement.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Strict.Runtime.Statements; - -public sealed class SetStatement(Instance instance, Register register) - : InstanceStatement(Instruction.Set, instance) -{ - public Register Register { get; } = register; - public override string ToString() => $"{base.ToString()} {Register}"; -} diff --git a/Strict.ndproj b/Strict.ndproj deleted file mode 100644 index e114fe13..00000000 --- a/Strict.ndproj +++ /dev/null @@ -1,13435 +0,0 @@ - - - .\NDependOut - - - - - - System.Runtime - System.Diagnostics.Debug - System.Collections - System.Runtime.Extensions - System.Linq - System.IO.FileSystem - mscorlib - - - C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.0.0 - C:\Program Files\dotnet\sdk\NuGetFallbackFolder - C:\Users\Administrator\.nuget\packages - C:\Users\Benjamin\.nuget\packages - - True - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1 - 1 - 0 - 0 - $ManDay$ - 50 - USD - After - 18 - 240 - 8 - 5 - 10 - 20 - 50 - 1200000000 - 12000000000 - 72000000000 - 360000000000 - - - - - Methods too complex - critical -warnif count > 0 from m in JustMyCode.Methods where - m.ILCyclomaticComplexity > 25 && - m.ILNestingDepth > 6 && - m.FullName != "NVorbis.VorbisResidue+Residue0.Decode(DataPacket,Boolean[],Int32,Int32)" && - m.Name != "GetSimpleName(String)" && - !m.Name.StartsWith("FindChildren") || - m.ILCyclomaticComplexity > 40 && - m.Name != ".ctor()" - orderby m.ILCyclomaticComplexity descending, m.ILNestingDepth descending -select new { m, m.ILCyclomaticComplexity, m.ILNestingDepth } - -// -// This rule matches methods where *CyclomaticComplexity* > 30 -// or *ILCyclomaticComplexity* > 60 -// or *ILNestingDepth* > 6. -// Such method is typically hard to understand and maintain. -// -// Maybe you are facing the **God Method** phenomenon. -// A "God Method" is a method that does way too many processes in the system -// and has grown beyond all logic to become *The Method That Does Everything*. -// When need for new processes increases suddenly some programmers realize: -// why should I create a new method for each processe if I can only add an *if*. -// -// See the definition of the *CyclomaticComplexity* metric here: -// http://www.ndepend.com/docs/code-metrics#CC -// -// See the definition of the *ILCyclomaticComplexity* metric here: -// http://www.ndepend.com/docs/code-metrics#ILCC -// -// See the definition of the *ILNestingDepth* metric here: -// http://www.ndepend.com/docs/code-metrics#ILNestingDepth -// - -// -// A large and complex method should be split in smaller methods, -// or even one or several classes can be created for that. -// -// During this process it is important to question the scope of each -// variable local to the method. This can be an indication if -// such local variable will become an instance field of the newly created class(es). -// -// Large *switch.case* structures might be refactored through the help -// of a set of types that implement a common interface, the interface polymorphism -// playing the role of the *switch cases tests*. -// -// Unit Tests can help: write tests for each method before extracting it -// to ensure you don't break functionality. -//]]> - Types too big - critical -warnif count > 0 from t in JustMyCode.Types where - t.Name != "Entity" && - t.Name != "EntityRenderer" && - t.Name != "Matrix" && - t.Name != "GLCore" && - t.Name != "Scene" && - t.Name != "AppRunner" && - t.Name != "SceneEntityRepository" && - !t.FullName.StartsWith("NVorbis") && - !t.Name.EndsWith("Tests") && - (t.NbLinesOfCode > 300 || - t.NbILInstructions > 2200) - orderby t.NbLinesOfCode descending -select new { t, t.NbLinesOfCode, t.NbILInstructions, t.Methods, t.Fields } - -// -// This rule matches types with more than 500 lines of code. -// -// Types where *NbLinesOfCode > 500* are extremely complex -// to develop and maintain. -// See the definition of the NbLinesOfCode metric here -// http://www.ndepend.com/docs/code-metrics#NbLinesOfCode -// -// Maybe you are facing the **God Class** phenomenon: -// A **God Class** is a class that controls way too many other classes -// in the system and has grown beyond all logic to become -// *The Class That Does Everything*. -// -// In average, a line of code is compiled to around -// 6 IL instructions. This is why the code metric -// *NbILInstructions* is used here, in case the -// code metric *NbLinesOfCode* is un-available because -// of missing assemblies corresponding PDB files. -// See the definition of the *NbILInstructions* metric here -// http://www.ndepend.com/docs/code-metrics#NbILInstructions -// - -// -// Types with many lines of code -// should be split in a group of smaller types. -// -// To refactor a *God Class* you'll need patience, -// and you might even need to recreate everything from scratch. -// Here are a few advices: -// -// - Think before pulling out methods: on what data does this method operate? -// What responsibility does it have? -// -// - Try to maintain the interface of the god class at first -// and delegate calls to the new extracted classes. -// In the end the god class should be a pure facade without own logic. -// Then you can keep it for convenience -// or throw it away and start to use the new classes only. -// -// - Unit Tests can help: write tests for each method before extracting it -// to ensure you don't break functionality. -//]]> - Methods with too many parameters - critical -warnif count > 0 from m in JustMyCode.Methods where - !m.ParentType.IsRecord && - !m.HasAttribute("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch()) && - !m.ParentType.Name.EndsWith("PropertyDescriptor") && - !m.ParentType.Name.EndsWith("Message") && - !m.ParentType.Name.EndsWith("Trigger") && - !m.ParentType.ParentNamespace.Name.EndsWith("Messages") && - !m.Name.StartsWith("On") && - !m.Name.StartsWith("Add") && - !m.Name.StartsWith("Create") && - !m.Name.StartsWith("FillEntity") && - m.NbParameters > 4 - - orderby m.NbParameters descending -select new { m, m.NbParameters, m.ParentType } - -// -// This rule matches methods with more than 8 parameters. -// Such method is painful to call and might degrade performance. -// See the definition of the *NbParameters* metric here: -// http://www.ndepend.com/docs/code-metrics#NbParameters -// - -// -// More properties/fields can be added to the declaring type to -// handle numerous states. An alternative is to provide -// a class or a structure dedicated to handle arguments passing. -// For example see the class *System.Diagnostics.ProcessStartInfo* -// and the method *System.Diagnostics.Process.Start(ProcessStartInfo)*. -//]]> - Methods too big -warnif count > 0 from m in JustMyCode.Methods where - m.Name != "GetPixelAverageColor(ColorImage,BoundingBox)" && - m.Name != "GetValidDistance(DepthData,Int32,Int32,Single,Single)" && - m.Name != "SearchForValidDistance(DepthData,Int32,Int32)" && - m.Name != "InitAlignFilterBlock()"&& - !m.Name.StartsWith("AddVertices") && - !m.Name.StartsWith("FillLineVertices") && - !m.Name.StartsWith("SaveIfIsPrimitiveData") && - !m.Name.StartsWith("LoadPrimitiveData") && - !m.Name.StartsWith("Copy") && - !m.FullName.Contains(".Scene") && - !m.FullName.Contains(".Frameworks") && - !m.IsClassConstructor && //should be ignored as some classes simply initialize a bunch of fields and this adds up, there is no code for this anyway - // should be mostly below 10, but NDepend is calculating this from the PDB, which adds some - // brackets and code we did not actually write (around 3 header + method block + 2 generated - // for blocks * 4, no method should be more complex than this upper limit). Some methods will - // slightly go above that and either need to be simplified and split up or should be excluded - // like above (if they are optimized methods, we should not have many of those). - m.NbLinesOfCode > 20+5 //see - orderby m.NbLinesOfCode descending, m.NbILInstructions descending -select new { m, lines=m.NbLinesOfCode-5, m.NbILInstructions } - -// -// This rule matches methods where *NbLinesOfCode > 30* or -// (commented per default) *NbILInstructions > 200*. -// Such method can be hard to understand and maintain. -// -// However rules like *Methods too complex* or *Methods with too many variables* -// might be more relevant to detect *painful to maintain* methods, -// because complexity is more related to numbers of *if*, -// *switch case*, loops. than to just number of lines. -// -// See the definition of the *NbLinesOfCode* metric here -// http://www.ndepend.com/docs/code-metrics#NbLinesOfCode -// - -// -// Usually too big methods should be split in smaller methods. -// -// But long methods with no branch conditions, that typically initialize some data, -// are not necessarily a problem to maintain nor to test, and might not need -// refactoring. -//IsClassConstructor]]> - Methods with too many parameters -warnif count > 0 from m in JustMyCode.Methods where - !m.ParentType.IsRecord && - !m.HasAttribute("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch()) && - !m.ParentType.Name.Contains("Renderer") && - !m.FullName.Contains("NVorbis") && - !m.FullName.Contains(".Frameworks") && - !m.FullName.Contains(".WebCam") && - m.ParentType.Name != "BinaryDataLoader" && - m.ParentType.Name != "Drawing" && - m.ParentType.Name != "CircularBuffer" && - m.ParentType.Name != "Drawing+RenderGeometry" && - m.ParentType.Name != "SpriteSheetAnimationUpdater" && - m.ParentType.Name != "InputTriggerUpdater" && - !m.Name.StartsWith("ReflectIfHittingBorder") && - !m.ParentType.Name.EndsWith("Template") && - !m.ParentType.Name.EndsWith("AssemblyExtensions") && - !m.ParentType.Name.StartsWith("BinaryData") && - !m.ParentType.Name.EndsWith("Tests") && - !m.Name.StartsWith("AddVertices") && - !m.Name.StartsWith("FillVerticesWithRotation(") && - !m.Name.StartsWith("FillLineVertices(") && - !m.Name.StartsWith("GetImageDifference(") && - !m.Name.StartsWith("CopyRgb16DataToRgba(") && - !m.Name.StartsWith("Create") && - !m.Name.StartsWith("FillEntity(") && - m.ParentType.Name != "GamePadButtonValueTrigger" && - m.ParentType.Name != "EntityProcessor" && - !m.Name.StartsWith("On") && - !m.Name.StartsWith(".ctor(") && - m.NbParameters > 5 - orderby m.NbParameters descending -select new { m, m.NbParameters } - -// -// This rule matches methods with more than 5 parameters. -// Such method might be painful to call and might degrade performance. -// See the definition of the *NbParameters* metric here: -// http://www.ndepend.com/docs/code-metrics#NbParameters -// - -// -// More properties/fields can be added to the declaring type to -// handle numerous states. An alternative is to provide -// a class or a structure dedicated to handle arguments passing. -// For example see the class *System.Diagnostics.ProcessStartInfo* -// and the method *System.Diagnostics.Process.Start(ProcessStartInfo))*. -//]]> - Methods with too many overloads -warnif count > 0 from m in JustMyCode.Methods where - m.NbOverloads > 6 && - !m.Name.Contains("Visit") && - !m.IsOperator // Don't report operator overload - orderby m.NbOverloads descending -let overloads = - m.IsConstructor ? m.ParentType.Constructors : - m.ParentType.Methods.Where(m1 => m1.SimpleName == m.SimpleName) -select new { m, overloads } - -// -// Method overloading is the ability to create multiple methods of the same name -// with different implementations, and various set of parameters. -// -// This rule matches sets of method with more than 6 overloads. -// -// Such method set might be a problem to maintain -// and provokes higher coupling than necessary. -// -// See the definition of the *NbOverloads* metric here -// http://www.ndepend.com/docs/code-metrics#NbOverloads -// - -// -// Typically the *too many overloads* phenomenon appears when an algorithm -// takes a various set of in-parameters. Each overload is presented as -// a facility to provide a various set of in-parameters. -// In such situation, the C# and VB.NET language feature named -// *Named and Optional arguments* should be used. -// -// The *too many overloads* phenomenon can also be a consequence of the usage -// of the **visitor design pattern** http://en.wikipedia.org/wiki/Visitor_pattern -// since a method named *Visit()* must be provided for each sub type. -// In such situation there is no need for fix. -//]]> - Types with too many methods -warnif count > 0 from t in JustMyCode.Types where - // Optimization: Fast discard of non-relevant types - t.Name != "Shader" && - t.Name != "Entity" && - t.Name != "EntityRenderer" && - t.Name != "SceneSerializer" && - t.Name != "Settings" && - t.Name != "EditorService" && - !t.Name.EndsWith("Tests") && - t.InstanceMethods.Count() > - (t.FullName.Contains("Extensions") || - t.FullName.Contains("Datatypes") || - t.FullName.Contains("Collections") || - t.FullName.Contains("Graphics") || - t.Name.EndsWith("Device") || - t.Name.EndsWith("Tests") || - t.Name.StartsWith("Test") || - t.Name == "ContentFileLoader" || - t.Name == "EntityRenderer" || - t.Name == "XmlData" || - t.Name == "TcpClient" || - t.Name == "Mouse" || - t.Name == "AppRunner" || - t.Name == "ReloadableDomainRunner" || - t.Name == "TcpSocket" || - t.Name.EndsWith("OnlineContentFileLoader") || - t.Name == "InParentDrawAreaTrigger" || - t.Name == "EntityInstance2D" || - t.Name == "EntityInstance3D" || - t.Name == "EntityRepository" || - t.Name == "BaseEntityRepository" || - t.Name == "SceneEntityRepository" || - t.Name == "SceneRepository" || - t.Name == "Scene" || - t.Name == "Resolver" || - t.Name == "Renderable" || - t.Name == "EngineTypesResolver" || - t.Name.EndsWith("Window") || - t.Name.EndsWith("View") || - t.Name.EndsWith("Commands") || - t.Name.EndsWith("RenderData") || - t.Name.StartsWith("RenderData") || - t.Name.EndsWith("SystemInformation") || - t.Name == "MicrosoftLogWriterAdapter" // Implementing interface has this many methods - ? 48 : 24) - - // Don't match these methods - let methods = t.Methods.Where( - m => !(m.IsGeneratedByCompiler || - m.IsConstructor || m.IsClassConstructor || - m.IsPropertyGetter || m.IsPropertySetter || - m.IsEventAdder || m.IsEventRemover)) - where methods.Count() > 20 - orderby methods.Count() descending -select new { t, - nbMethods = methods.Count(), - instanceMethods = methods.Where(m => !m.IsStatic), - staticMethods = methods.Where(m => m.IsStatic)} - -// -// This rule matches types with more than 20 methods. -// Such type might be hard to understand and maintain. -// -// Notice that methods like constructors or property -// and event accessors are not taken account. -// -// Having many methods for a type might be a symptom -// of too many responsibilities implemented. -// -// Maybe you are facing the **God Class** phenomenon: -// A **God Class** is a class that controls way too many other classes -// in the system and has grown beyond all logic to become -// *The Class That Does Everything*. -// - -// -// To refactor such type and increase code quality and maintainability, -// certainly you'll have to split the type into several smaller types -// that together, implement the same logic. -// -// To refactor a *God Class* you'll need patience, -// and you might even need to recreate everything from scratch. -// Here are a few advices: -// -// - Think before pulling out methods: -// What responsibility does it have? -// Can you isolate some subsets of methods that operate on the same subsets of fields? -// -// - Try to maintain the interface of the god class at first -// and delegate calls to the new extracted classes. -// In the end the god class should be a pure facade without own logic. -// Then you can keep it for convenience -// or throw it away and start to use the new classes only. -// -// - Unit Tests can help: write tests for each method before extracting it -// to ensure you don't break functionality. -//]]> - Types with too many fields -warnif count > 0 from t in JustMyCode.Types - - // Optimization: Fast discard of non-relevant types - where !t.IsEnumeration && - t.Fields.Count() > 20 && - !t.FullName.Contains(".Scene") - - // Count instance fields and non-constant static fields - let fields = t.Fields.Where(f => - !f.IsGeneratedByCompiler && - !f.IsLiteral && - !(f.IsStatic && f.IsInitOnly) && - JustMyCode.Contains(f) ) - - where fields.Count() > 20 - - orderby fields.Count() descending -select new { t, - instanceFields = fields.Where(f => !f.IsStatic), - staticFields = fields.Where(f => f.IsStatic), - - // See definition of Size of Instances metric here: - // http://www.ndepend.com/docs/code-metrics#SizeOfInst - t.SizeOfInst -} - -// -// This rule matches types with more than 20 fields. -// Such type might be hard to understand and maintain. -// -// Notice that constant fields and static-readonly fields are not counted. -// Enumerations types are not counted also. -// -// Having many fields for a type might be a symptom -// of too many responsibilities implemented. -// - -// -// To refactor such type and increase code quality and maintainability, -// certainly you'll have to group subsets of fields into smaller types -// and dispatch the logic implemented into the methods -// into these smaller types. -//]]> - Types with poor cohesion -warnif count > 0 from t in JustMyCode.Types where - (t.LCOM > 0.91 || t.LCOMHS > 0.94) && - t.NbFields > 12 && - t.NbMethods > 12 && - t.Name != "Mouse" && - !t.Name.EndsWith("ContentFileLoader") && - t.Name != "OnlineServiceConnection" && - t.Name != "SetDataPropertyProcessor" && - t.Name != "AssemblyTypeLoader" && - t.Name != "AppRunner" && - t.Name != "MainWindow" && - t.Name != "RegisterScene" && - t.Name != "TcpSocket" && - t.Name != "EntityProcessor" && - t.Name != "EditorService" && - t.Name != "Entity" && - !t.FullName.Contains("NVorbis") && - !t.FullName.Contains(".Frameworks") && - !t.FullName.Contains(".WebCam") - orderby t.LCOM descending, t.LCOMHS descending -select new { t, t.LCOM, t.LCOMHS, - t.NbMethods, t.NbFields } - -// -// This rule is based on the *LCOM code metric*, -// LCOM stands for **Lack Of Cohesion of Methods**. -// See the definition of the LCOM metric here -// http://www.ndepend.com/docs/code-metrics#LCOM -// -// The LCOM metric measures the fact that most methods are using most fields. -// A class is considered utterly cohesive (which is good) -// if all its methods use all its instance fields. -// -// Only types with enough methods and fields are taken account to avoid bias. -// The LCOM takes its values in the range [0-1]. -// -// This rule matches types with LCOM higher than 0.8. -// Such value generally pinpoints a **poorly cohesive class**. -// -// There are several LCOM metrics. -// The LCOM HS (HS stands for Henderson-Sellers) takes its values in the range [0-2]. -// A LCOM HS value higher than 1 should be considered alarming. -// - -// -// To refactor a poorly cohesive type and increase code quality and maintainability, -// certainly you'll have to split the type into several smaller and more cohesive types -// that together, implement the same logic. -//]]> - - - Quality Gates Evolution -from qg in QualityGates -let qgBaseline = qg.OlderVersion() -let relyOnDiff = qgBaseline == null -let evolution = relyOnDiff ? (TrendIcon?)null : - // When a quality gate relies on diff between now and baseline - // it is not executed against the baseline - qg.ValueDiff() == 0d ? - TrendIcon.Constant : - (qg.ValueDiff() > 0 ? - ( qg.MoreIsBad ? TrendIcon.RedUp: TrendIcon.GreenUp) : - (!qg.MoreIsBad ? TrendIcon.RedDown: TrendIcon.GreenDown)) -select new { qg, - Evolution = evolution, - - BaselineStatus = relyOnDiff? (QualityGateStatus?) null : qgBaseline.Status, - Status = qg.Status, - - BaselineValue = relyOnDiff? (null) : qgBaseline.ValueString, - Value = qg.ValueString, -} - -// -// Show quality gates evolution between baseline and now. -// -// When a quality gate relies on diff between now and baseline (like *New Debt since Baseline*) -// it is not executed against the baseline and as a consequence its evolution is not available. -// -// Double-click a quality gate for editing. -// ]]> - -failif value < 70% -warnif value < 80% -codeBase.PercentageCoverage - -// -// Code coverage is a measure used to describe the degree to which the source code of a program -// is tested by a particular test suite. A program with high code coverage, measured as a percentage, -// has had more of its source code executed during testing which suggests it has a lower chance of -// containing undetected software bugs compared to a program with low code coverage. -// -// Code coverage is certainly the most important quality code metric. But coverage is not enough -// the team needs to ensure that results are checked at test-time. These checks can be done both -// in test code, and in application code through assertions. The important part is that a test -// must fail explicitely when a check gets unvalidated during the test execution. -// -// This quality gate define a warn threshold (70%) and a fail threshold (80%). These are -// indicative thresholds and in practice the more the better. To achieve high coverage and -// low risk, make sure that new and refactored classes gets 100% covered by tests and that -// the application and test code contains as many checks/assertions as possible. -//]]> - -failif value < 70% -warnif value < 80% -let newMethods = Application.Methods.Where(m => m.WasAdded() && m.NbLinesOfCode > 0) -let locCovered = newMethods.Sum(m => m.NbLinesOfCodeCovered) -let loc = newMethods.Sum(m => m.NbLinesOfCode) -select 100d * locCovered / loc - -// -// *New Code* is defined as methods added since the baseline. -// -// To achieve high code coverage it is essential that new code gets properly -// tested and covered by tests. It is advised that all non-UI new classes gets -// 100% covered. -// -// Typically 90% of a class is easy to cover by tests and 10% is hard to reach -// through tests. It means that this 10% remaining is not easily testable, which -// means it is not well designed, which often means that this code is especially -// **error-prone**. This is the reason why it is important to reach 100% coverage -// for a class, to make sure that potentially *error-prone* code gets tested. -// -]]> - -failif value < 70% -warnif value < 80% -let newMethods = Application.Methods.Where(m => m.CodeWasChanged() && m.NbLinesOfCode > 0) -let locCovered = newMethods.Sum(m => m.NbLinesOfCodeCovered) -let loc = newMethods.Sum(m => m.NbLinesOfCode) -select 100d * locCovered / loc - -// -// *Refactored Code* is defined as methods where *code was changed* since the baseline. -// -// Comment changes and formatting changes are not considerd as refactoring. -// -// To achieve high code coverage it is essential that refactored code gets properly -// tested and covered by tests. It is advised that when refactoring a class -// or a method, it is important to also write tests to make sure it gets 100% covered. -// -// Typically 90% of a class is easy to cover by tests and 10% is hard to reach -// through tests. It means that this 10% remaining is not easily testable, which -// means it is not well designed, which often means that this code is especially -// **error-prone**. This is the reason why it is important to reach 100% coverage -// for a class, to make sure that potentially *error-prone* code gets tested. -// -]]> - -failif count > 0 issues -from i in Issues -where i.Severity == Severity.Blocker -select new { i, i.Severity, i.Debt, i.AnnualInterest } - -// -// An issue with the severity **Blocker** cannot move to production, it must be fixed. -// -// The severity of an issue is either defined explicitely in the rule source code, -// either inferred from the issue *annual interest* and thresholds defined in the -// NDepend Project Properties > Issue and Debt. -// - -]]> - -failif count > 10 issues -warnif count > 0 issues - -from i in Issues -where i.Severity == Severity.Critical -select new { i, i.Severity, i.Debt, i.AnnualInterest } - -// -// An issue with a severity level **Critical** shouldn't move to production. -// It still can for business imperative needs purposes, but at worst it must -// be fixed during the next iterations. -// -// The severity of an issue is either defined explicitely in the rule source code, -// either inferred from the issue *annual interest* and thresholds defined in the -// NDepend Project Properties > Issue and Debt. -//]]> - -failif count > 0 issues -from i in Issues -where i.Severity.EqualsAny(Severity.Blocker, Severity.Critical, Severity.High) && - // Count both the new issues and the issues that became at least Critical - (i.WasAdded() || i.OlderVersion().Severity < Severity.High) -select new { i, i.Severity, i.Debt, i.AnnualInterest } - - -// -// An issue with the severity **Blocker** cannot move to production, it must be fixed. -// -// An issue with a severity level **Critical** shouldn't move to production. -// It still can for business imperative needs purposes, but at worth it must be fixed -// during the next iterations. -// -// An issue with a severity level **High** should be fixed quickly, but can wait until -// the next scheduled interval. -// -// The severity of an issue is either defined explicitely in the rule source code, -// either inferred from the issue *annual interest* and thresholds defined in the -// NDepend Project Properties > Issue and Debt. -// -]]> - -failif count > 0 rules -from r in Rules where r.IsCritical && r.IsViolated() -select new { r, issues = r.Issues() } - -// -// The concept of critical rule is useful to pinpoint certain rules that -// should not be violated. -// -// A rule can be made critical just by checking the *Critical button* in the -// rule edition control and then saving the rule. -// -// This quality gate fails if any critical rule gets any violations. -// -// When no baseline is available, rules that rely on diff are not counted. -// If you observe that this quality gate count slightly decreases with no apparent reason, -// the reason is certainly that rules that rely on diff are not counted -// because the baseline is not defined. -//]]> - -failif value > 30% -warnif value > 20% -let timeToDev = codeBase.EffortToDevelop() -let debt = Issues.Sum(i => i.Debt) -select 100d * debt.ToManDay() / timeToDev.ToManDay() - -// -// % Debt total is defined as a percentage on: -// -// • the estimated total effort to develop the code base -// -// • and the the estimated total time to fix all issues (the Debt) -// -// Estimated total effort to develop the code base is inferred from -// # lines of code of the code base and from the -// *Estimated number of man-day to develop 1000 logicial lines of code* -// setting found in NDepend Project Properties > Issue and Debt. -// -// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt -// -// This quality gates fails if the estimated debt is more than 30% -// of the estimated effort to develop the code base, and warns if the -// estimated debt is more than 20% of the estimated effort to develop -// the code base -// ]]> - -failif value > 50 man-days -warnif value > 30 man-days -Issues.Sum(i => i.Debt).ToManDay() - -// -// This Quality Gate is disabled per default because the fail and warn -// thresholds of unacceptable Debt in man-days can only depend on the -// project size, number of developers and overall context. -// -// However you can refer to the default Quality Gate **Percentage Debt**. -// -// The Debt is defined as the sum of estimated effort to fix all issues. -// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt -//]]> - -failif value > 2 man-days -warnif value > 0 man-days -let debt = Issues.Sum(i => i.Debt) -let debtInBaseline = IssuesInBaseline.Sum(i => i.Debt) -select (debt - debtInBaseline).ToManDay() - - -// -// This Quality Gate fails if the estimated effort to fix new or worsened -// issues (what is called the *New Debt since Baseline*) is higher -// than 2 man-days. -// -// This Quality Gate warns if this estimated effort is positive. -// -// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt -//]]> - -failif count > 0 namespaces - -from n in Application.Namespaces -where n.DebtRating() != null && - n.DebtRating().Value.EqualsAny(DebtRating.E, DebtRating.D) -select new { - n, - debtRating = n.DebtRating(), - debtRatio = n.DebtRatio(), // % of debt from which DebtRating is inferred - devTimeInManDay = n.EffortToDevelop().ToDebt(), - debtInManDay = n.AllDebt(), - issues = n.AllIssues() -} - -// -// Forbid namespaces with a poor Debt Rating equals to **E** or **D**. -// -// The **Debt Rating** for a code element is estimated by the value of the **Debt Ratio** -// and from the various rating thresholds defined in this project *Debt Settings*. -// -// The **Debt Ratio** of a code element is a percentage of **Debt Amount** (in floating man-days) -// compared to the **estimated effort to develop the code element** (also in floating man-days). -// -// The **estimated effort to develop the code element** is inferred from the code elements -// number of lines of code, and from the project *Debt Settings* parameters -// *estimated number of man-days to develop 1000* **logical lines of code**. -// -// The **logical lines of code** corresponds to the number of debug breakpoints in a method -// and doesn't depend on code formatting nor comments. -// -// The Quality Gate can be modified to match assemblies, types or methods -// with a poor Debt Rating, instead of matching namespaces. -// ]]> - -failif value > 50 man-days -warnif value > 30 man-days -Issues.Sum(i => i.AnnualInterest).ToManDay() - - -// -// This Quality Gate is disabled per default because the fail and warn -// thresholds of unacceptable Annual-Interest in man-days can only depend -// on the project size, number of developers and overall context. -// -// However you can refer to the default Quality Gate -// **New Annual Interest since Baseline**. -// -// The Annual-Interest is defined as the sum of estimated annual cost -// in man-days, to leave all issues unfixed. -// -// Each rule can either provide a formula to compute the Annual-Interest -// per issue, or assign a **Severity** level for each issue. Some thresholds -// defined in *Project Properties > Issue and Debt > Annual Interest* are -// used to infer an Annual-Interest value from a Severity level. -// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest -//]]> - -failif value > 2 man-days -warnif value > 0 man-days -let ai = Issues.Sum(i => i.AnnualInterest) -let aiInBaseline = IssuesInBaseline.Sum(i => i.AnnualInterest) -select (ai - aiInBaseline).ToManDay() - -// -// This Quality Gate fails if the estimated annual cost to leave all issues -// unfixed, increased from more than 2 man-days since the baseline. -// -// This Quality Gate warns if this estimated annual cost is positive. -// -// This estimated annual cost is named the **Annual-Interest**. -// -// Each rule can either provide a formula to compute the Annual-Interest -// per issue, or assign a **Severity** level for each issue. Some thresholds -// defined in *Project Properties > Issue and Debt > Annual Interest* are -// used to infer an Annual-Interest value from a Severity level. -// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest -//]]> - - - Types Hot Spots -from t in JustMyCode.Types -where t.AllDebt() > Debt.Zero && - t.AllAnnualInterest() > AnnualInterest.Zero -orderby t.AllDebt().Value.TotalMinutes descending -select new { t, - Debt = t.AllDebt(), - Issues = t.AllIssues(), // AllIssues = {types issues} union {members issues} - AnnualInterest = t.AllAnnualInterest(), - BreakingPoint = t.AllBreakingPoint(), - t.NbLinesOfCode, - // t.PercentageCoverage, to uncomment if coverage data is imported - DebtRating = t.DebtRating(), - DebtRatio = t.DebtRatio() -} - -// -// This query lists **types with most Debt**, -// or in other words, types with issues that would need -// the largest effort to get fixed. -// -// Both issues on the type and its members are -// taken account. -// -// Since untested code often generates a lot of -// Debt, the type size and percentage coverage is shown -// (just uncomment *t.PercentageCoverage* in the query -// source code once you've imported the coverage data). -// -// The *Debt Rating* and *Debt Ratio* are also shown -// for informational purpose. -// -// -- -// -// The amount of *Debt* is not a measure to prioritize -// the effort to fix issues, it is an estimation of how far -// the team is from clean code that abides by the rules set. -// -// For each issue the *Annual Interest* estimates the annual -// cost to leave the issues unfixed. The *Severity* of an issue -// is estimated through thresholds from the *Annual Interest*. -// -// The **Debt Breaking Point** represents the duration -// from now when the estimated cost to leave the issue unfixed -// costs as much as the estimated effort to fix it. -// -// Hence the shorter the **Debt Breaking Point** -// the largest the **Return on Investment** for fixing -// the issue. The **Breaking Point is the right metric -// to prioritize issues fix**. -//]]> - Types to Fix Priority -from t in JustMyCode.Types -where t.AllBreakingPoint() > TimeSpan.Zero && - t.AllDebt().Value > 30.ToMinutes() -orderby t.AllBreakingPoint().TotalMinutes ascending -select new { t, - BreakingPoint = t.AllBreakingPoint(), - Debt = t.AllDebt(), - AnnualInterest = t.AllAnnualInterest(), - Issues = t.AllIssues(), - t.NbLinesOfCode, - // t.PercentageCoverage, to uncomment if coverage data is imported - DebtRating = t.DebtRating(), - DebtRatio = t.DebtRatio() -} - -// -// This query lists types per increasing -// **Debt Breaking Point**. -// -// For each issue the *Debt* estimates the -// effort to fix the issue, and the *Annual Interest* -// estimates the annual cost to leave the issue unfixed. -// The *Severity* of an issue is estimated through -// thresholds from the *Annual Interest* of the issue. -// -// The **Debt Breaking Point** represents the duration -// from now when the estimated cost to leave the issue unfixed -// costs as much as the estimated effort to fix it. -// -// Hence the shorter the **Debt Breaking Point** -// the largest the **Return on Investment** for fixing -// the issues. -// -// Often new and refactored types since baseline will be -// listed first, because issues on these types get a -// higher *Annual Interest* because it is important to -// focus first on new issues. -// -// -// -- -// -// Both issues on the type and its members are -// taken account. -// -// Only types with at least 30 minutes of Debt are listed -// to avoid parasiting the list with the numerous -// types with small *Debt*, on which the *Breaking Point* -// value makes less sense. -// -// The *Annual Interest* estimates the cost per year -// in man-days to leave these issues unfixed. -// -// Since untested code often generates a lot of -// Debt, the type size and percentage coverage is shown -// (just uncomment *t.PercentageCoverage* in the query -// source code once you've imported the coverage data). -// -// The *Debt Rating* and *Debt Ratio* are also shown -// for informational purpose. -//]]> - Issues to Fix Priority -from i in Issues -// Don't show first issues with BreakingPoint equals to zero. -orderby i.BreakingPoint != TimeSpan.Zero ? i.BreakingPoint : TimeSpan.MaxValue -select new { i, - Debt = i.Debt, - AnnualInterest = i.AnnualInterest, - BreakingPoint = i.BreakingPoint, - CodeElement = i.CodeElement -} - -// -// This query lists issues per increasing -// **Debt Breaking Point**. -// -// Double-click an issue to edit its rule and -// select the issue in the rule result. This way -// you can view all information concerning the issue. -// -// For each issue the *Debt* estimates the -// effort to fix the issue, and the *Annual Interest* -// estimates the annual cost to leave the issue unfixed. -// The *Severity* of an issue is estimated through -// thresholds from the *Annual Interest* of the issue. -// -// The **Debt Breaking Point** represents the duration -// from now when the estimated cost to leave the issue unfixed -// costs as much as the estimated effort to fix it. -// -// Hence the shorter the **Debt Breaking Point** -// the largest the **Return on Investment** for fixing -// the issue. -// -// Often issues on new and refactored code elements since -// baseline will be listed first, because such issues get a -// higher *Annual Interest* because it is important to -// focus first on new issues on recent code. -// -// More documentation: https://www.ndepend.com/docs/technical-debt -//]]> - Debt and Issues per Rule -from r in Rules -where r.IsViolated() -orderby r.Debt().Value descending -select new { - r, - Issues = r.Issues(), - Debt = r.Debt(), - AnnualInterest = r.AnnualInterest(), - BreakingPoint = r.BreakingPoint(), - Category = r.Category -} - -// -// This query lists violated rules with most *Debt* first. -// -// A rule violated has issues. For each issue the *Debt* -// estimates the effort to fix the issue. -// -// -- -// -// The amount of *Debt* is not a measure to prioritize -// the effort to fix issues, it is an estimation of how far -// the team is from clean code that abides by the rules set. -// -// For each issue the *Annual Interest* estimates the annual -// cost to leave the issues unfixed. The *Severity* of an issue -// is estimated through thresholds from the *Annual Interest*. -// -// The **Debt Breaking Point** represents the duration -// from now when the estimated cost to leave the issue unfixed -// costs as much as the estimated effort to fix it. -// -// Hence the shorter the **Debt Breaking Point** -// the largest the **Return on Investment** for fixing -// the issue. The **Breaking Point is the right metric -// to prioritize issues fix**. -// -// -- -// -// Notice that rules can be grouped in *Rule Category*. This -// way you'll see categories that generate most *Debt*. -// -// Typically the rules that generate most *Debt* are the -// ones related to *Code Coverage by Tests*, *Architecture* -// and *Code Smells*. -// -// More documentation: https://www.ndepend.com/docs/technical-debt -//]]> - New Debt and Issues per Rule -from r in Rules -where r.IsViolated() && r.IssuesAdded().Count() > 0 -orderby r.DebtDiff().Value descending -select new { - r, - IssuesAdded = r.IssuesAdded(), - IssuesFixed = r.IssuesFixed(), - Issues = r.Issues(), - Debt = r.Debt(), - DebtDiff = r.DebtDiff(), - Category = r.Category -} - -// -// This query lists violated rules that have new issues -// since baseline, with most **new Debt** first. -// -// A rule violated has issues. For each issue the *Debt* -// estimates the effort to fix the issue. -// -// -- -// -// New issues since the baseline are consequence of recent code -// refactoring sessions. They represent good opportunities -// of fix because the code recently refactored is fresh in -// the developers mind, which means fixing now costs less -// than fixing later. -// -// Fixing issues on recently touched code is also a good way -// to foster practices that will lead to higher code quality -// and maintainability, including writing unit-tests -// and avoiding unnecessary complex code. -// -// -- -// -// Notice that rules can be grouped in *Rule Category*. This -// way you'll see categories that generate most *Debt*. -// -// Typically the rules that generate most *Debt* are the -// ones related to *Code Coverage by Tests*, *Architecture* -// and *Code Smells*. -// -// More documentation: https://www.ndepend.com/docs/technical-debt -//]]> - Debt and Issues per Code Element -from elem in CodeElements -where elem.HasIssue() -orderby elem.Debt().Value descending -select new { - elem, - Issues = elem.Issues(), - Debt = elem.Debt(), - AnnualInterest = elem.AnnualInterest(), - BreakingPoint = elem.BreakingPoint() -} - -// -// This query lists code elements that have issues, -// with most *Debt* first. -// -// For each code element the *Debt* estimates -// the effort to fix the element issues. -// -// The amount of *Debt* is not a measure to prioritize -// the effort to fix issues, it is an estimation of how far -// the team is from clean code that abides by the rules set. -// -// For each element the *Annual Interest* estimates the annual -// cost to leave the elements issues unfixed. The *Severity* of an -// issue is estimated through thresholds from the *Annual Interest* -// of the issue. -// -// The **Debt Breaking Point** represents the duration -// from now when the estimated cost to leave the issues unfixed -// costs as much as the estimated effort to fix it. -// -// Hence the shorter the **Debt Breaking Point** -// the largest the **Return on Investment** for fixing -// the issue. The **Breaking Point is the right metric -// to prioritize issues fix**. -//]]> - New Debt and Issues per Code Element -from elem in CodeElements -where elem.HasIssue() && elem.IssuesAdded().Count() > 0 -orderby elem.DebtDiff().Value descending -select new { - elem, - IssuesAdded = elem.IssuesAdded(), - IssuesFixed = elem.IssuesFixed(), - Issues = elem.Issues(), - Debt = elem.Debt(), - DebtDiff = elem.DebtDiff() -} - // -// This query lists code elements that have new issues -// since baseline, with most **new Debt** first. -// -// For each code element the *Debt* estimates -// the effort to fix the element issues. -// -// New issues since the baseline are consequence of recent code -// refactoring sessions. They represent good opportunities -// of fix because the code recently refactored is fresh in -// the developers mind, which means fixing now costs less -// than fixing later. -// -// Fixing issues on recently touched code is also a good way -// to foster practices that will lead to higher code quality -// and maintainability, including writing unit-tests -// and avoiding unnecessary complex code. -// -]]> - - - Avoid types too big -// ND1000:AvoidTypesTooBig -warnif count > 0 from t in JustMyCode.Types where - - // First filter on type to optimize - t.NbLinesOfCode > 200 && - !t.NameLike("Render") && - !t.NameLike("Scene") && - !t.NameLike("BinaryData") && - !t.NameLike("Component") && - !t.NameLike("Xml") && - !t.NameLike("GLCore") && - !t.NameLike("VorbisStreamDecoder") && - !t.NameLike("Mdct") && - !t.NameLike("VorbisFloor") && - !t.NameLike("App") && - !t.NameLike("Webcam") && - !t.NameLike("Assembly") && - !t.NameLike("TriggerIsInvoked") && - !t.NameLike("StackTrace") && - !t.NameLike("Drawing") - - // # IL Instructions is commented, because with LINQ syntax, a few lines of code can compile to hundreds of IL instructions. - // || t.NbILInstructions > 3000 - - // What matters is the # lines of code in JustMyCode - let locJustMyCode = t.MethodsAndConstructors.Where(m => JustMyCode.Contains(m)).Sum(m => m.NbLinesOfCode) - where locJustMyCode > 200 - - let isStaticWithNoMutableState = (t.IsStatic && t.Fields.Any(f => !f.IsImmutable)) - let staticFactor = (isStaticWithNoMutableState ? 0.2 : 1) - - orderby locJustMyCode descending -select new { - t, - locJustMyCode, - t.NbILInstructions, - t.Methods, - t.Fields, - - Debt = (staticFactor*locJustMyCode.Linear(200, 1, 2000, 10)).ToHours().ToDebt(), - - // The annual interest varies linearly from interest for severity major for 300 loc - // to interest for severity critical for 2000 loc - AnnualInterest = staticFactor*(locJustMyCode.Linear( - 200, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 2000, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() -} - -// -// This rule matches types with more than 200 lines of code. -// **Only lines of code in JustMyCode methods are taken account.** -// -// Types where *NbLinesOfCode > 200* are extremely complex -// to develop and maintain. -// See the definition of the NbLinesOfCode metric here -// https://www.ndepend.com/docs/code-metrics#NbLinesOfCode -// -// Maybe you are facing the **God Class** phenomenon: -// A **God Class** is a class that controls way too many other classes -// in the system and has grown beyond all logic to become -// *The Class That Does Everything*. -// - -// -// Types with many lines of code -// should be split in a group of smaller types. -// -// To refactor a *God Class* you'll need patience, -// and you might even need to recreate everything from scratch. -// Here are a few refactoring advices: -// -// • The logic in the *God Class* must be splitted in smaller classes. -// These smaller classes can eventually become private classes nested -// in the original *God Class*, whose instances objects become -// composed of instances of smaller nested classes. -// -// • Smaller classes partitioning should be driven by the multiple -// responsibilities handled by the *God Class*. To identify these -// responsibilities it often helps to look for subsets of methods -// strongly coupled with subsets of fields. -// -// • If the *God Class* contains way more logic than states, a good -// option can be to define one or several static classes that -// contains no static field but only pure static methods. A pure static -// method is a function that computes a result only from inputs -// parameters, it doesn't read nor assign any static or instance field. -// The main advantage of pure static methods is that they are easily -// testable. -// -// • Try to maintain the interface of the *God Class* at first -// and delegate calls to the new extracted classes. -// In the end the *God Class* should be a pure facade without its own logic. -// Then you can keep it for convenience or throw it away and -// start to use the new classes only. -// -// • Unit Tests can help: write tests for each method before extracting it -// to ensure you don't break functionality. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 1 hour for a 200 lines of code type, -// up to 10 hours for a type with 2.000 or more lines of code. -// -// In Debt and Interest computation, this rule takes account of the fact -// that static types with no mutable fields are just a collection of -// static methods that can be easily splitted and moved from one type -// to another. -//]]> - Avoid types with too many methods -// ND1001:AvoidTypesWithTooManyMethods -warnif count > 0 from t in JustMyCode.Types - - // Optimization: Fast discard of non-relevant types - where t.Methods.Count() > 20 - - // Don't match these methods - let methods = t.Methods.Where( - m => !(m.IsGeneratedByCompiler || - // m.IsConstructor || m.IsClassConstructor || // ctor/cctor not enumerated through IType.Methods - m.IsPropertyGetter || m.IsPropertySetter || - m.IsEventAdder || m.IsEventRemover)) - - where methods.Count() > 20 - orderby methods.Count() descending - - let isStaticWithNoMutableState = (t.IsStatic && t.Fields.Any(f => !f.IsImmutable)) - let staticFactor = (isStaticWithNoMutableState ? 0.2 : 1) - -select new { - t, - nbMethods = methods.Count(), - instanceMethods = methods.Where(m => !m.IsStatic), - staticMethods = methods.Where(m => m.IsStatic), - - t.NbLinesOfCode, - - Debt = (staticFactor*methods.Count().Linear(20, 1, 200, 10)).ToHours().ToDebt(), - - // The annual interest varies linearly from interest for severity major for 30 methods - // to interest for severity critical for 200 methods - AnnualInterest = (staticFactor*methods.Count().Linear( - 20, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 200, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() -} - -// -// This rule matches types with more than 20 methods. -// Such type might be hard to understand and maintain. -// -// Notice that methods like constructors or property -// and event accessors are not taken account. -// -// Having many methods for a type might be a symptom -// of too many responsibilities implemented. -// -// Maybe you are facing the **God Class** phenomenon: -// A **God Class** is a class that controls way too many other classes -// in the system and has grown beyond all logic to become -// *The Class That Does Everything*. -// - -// -// To refactor properly a *God Class* please read *HowToFix advices* -// from the default rule **Types to Big**. -//// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 1 hour for a type with 20 methods, -// up to 10 hours for a type with 200 or more methods. -// -// In Debt and Interest computation, this rule takes account of the fact -// that static types with no mutable fields are just a collection of -// static methods that can be easily splitted and moved from one type -// to another. -//]]> - Avoid types with too many fields -// ND1002:AvoidTypesWithTooManyFields -warnif count > 0 from t in JustMyCode.Types - - // Optimization: Fast discard of non-relevant types - where !t.IsEnumeration && - t.Fields.Count() > 15 && - !t.FullName.Contains("NVorbis") && - !t.FullName.Contains("GLCore") && - !t.FullName.Contains("Matrix") && - !t.FullName.Contains("AppRunner") && - !t.FullName.Contains("Drawing") && - !t.FullName.Contains("TextWrapper") && - !t.FullName.Contains("WglGraphicsContext") && - !t.FullName.Contains("PixelFormatDescriptor") && - !t.FullName.Contains("WindowsGamePad") && - !t.FullName.Contains("SetDataPropertyProcessor") && - !t.FullName.Contains("WebCam") && - !t.FullName.Contains("Strict.Robotics.Controller") - - - // Count instance fields and non-constant static fields - let fields = t.Fields.Where(f => - !f.IsGeneratedByCompiler && - !f.IsLiteral && - !(f.IsStatic && f.IsInitOnly) && - JustMyCode.Contains(f) ) - - where fields.Count() > 15 - - let methodsAssigningFields = fields.SelectMany(f => f.MethodsAssigningMe) - - orderby fields.Count() descending -select new { - t, - instanceFields = fields.Where(f => !f.IsStatic), - staticFields = fields.Where(f => f.IsStatic), -methodsAssigningFields , - - // See definition of Size of Instances metric here: - // https://www.ndepend.com/docs/code-metrics#SizeOfInst - t.SizeOfInst, - - Debt = fields.Count().Linear(15, 1, 200, 10).ToHours().ToDebt(), - - // The annual interest varies linearly from interest for severity major for 30 methods - // to interest for severity critical for 200 methods - AnnualInterest = fields.Count().Linear(15, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 200, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest() -} - -// -// This rule matches types with more than 15 fields. -// Such type might be hard to understand and maintain. -// -// Notice that constant fields and static-readonly fields are not counted. -// Enumerations types are not counted also. -// -// Having many fields for a type might be a symptom -// of too many responsibilities implemented. -// - -// -// To refactor such type and increase code quality and maintainability, -// certainly you'll have to group subsets of fields into smaller types -// and dispatch the logic implemented into the methods -// into these smaller types. -// -// More refactoring advices can be found in the default rule -// **Types to Big**, *HowToFix* section. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 1 hour for a type with 15 fields, -// to up to 10 hours for a type with 200 or more fields. -//]]> - Avoid methods too big, too complex -// ND1003:AvoidMethodsTooBigTooComplex -warnif count > 0 from m in JustMyCode.Methods where - - // Don't match async methods here to avoid - // false positives because of special compiler tricks. - !m.IsAsync && - - m.ILNestingDepth > 2 && - !m.IsClassConstructor && - m.FullName.StartsWith("Webcam") && - (m.NbLinesOfCode > 35 || - m.CyclomaticComplexity > 20 || - m.ILCyclomaticComplexity > 60) - - let complexityScore = m.NbLinesOfCode/2 + m.CyclomaticComplexity + m.ILCyclomaticComplexity/3 + 3*m.ILNestingDepth - - orderby complexityScore descending, - m.CyclomaticComplexity descending, - m.ILCyclomaticComplexity descending, - m.ILNestingDepth descending -select new { - m, - m.NbLinesOfCode, - m.CyclomaticComplexity, - m.ILCyclomaticComplexity, - m.ILNestingDepth, - complexityScore, - - Debt = complexityScore.Linear(30, 40, 400, 8*60).ToMinutes().ToDebt(), - - // The annual interest varies linearly from interest for severity minor - // to interest for severity major - AnnualInterest = complexityScore .Linear(30, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 200, 2*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() - -} - -// -// This rule matches methods where *ILNestingDepth* > 2 -// and (*NbLinesOfCode* > 35 -// or *CyclomaticComplexity* > 20 -// or *ILCyclomaticComplexity* > 60) -// Such method is typically hard to understand and maintain. -// -// Maybe you are facing the **God Method** phenomenon. -// A "God Method" is a method that does way too many processes in the system -// and has grown beyond all logic to become *The Method That Does Everything*. -// When need for new processes increases suddenly some programmers realize: -// why should I create a new method for each processe if I can only add an *if*. -// -// See the definition of the *CyclomaticComplexity* metric here: -// https://www.ndepend.com/docs/code-metrics#CC -// -// See the definition of the *ILCyclomaticComplexity* metric here: -// https://www.ndepend.com/docs/code-metrics#ILCC -// -// See the definition of the *ILNestingDepth* metric here: -// https://www.ndepend.com/docs/code-metrics#ILNestingDepth -// - -// -// A large and complex method should be split in smaller methods, -// or even one or several classes can be created for that. -// -// During this process it is important to question the scope of each -// variable local to the method. This can be an indication if -// such local variable will become an instance field of the newly created class(es). -// -// Large *switch…case* structures might be refactored through the help -// of a set of types that implement a common interface, the interface polymorphism -// playing the role of the *switch cases tests*. -// -// Unit Tests can help: write tests for each method before extracting it -// to ensure you don't break functionality. -// -// The estimated Debt, which means the effort to fix such issue, -// varies from 40 minutes to 8 hours, linearly from a weighted complexity score. -//]]> - Avoid methods with too many parameters -// ND1004:AvoidMethodsWithTooManyParameters -warnif count > 0 from m in JustMyCode.Methods where - !m.ParentType.IsRecord && - m.NbParameters >= 7 && - !m.Parent.NameLike("2D") && - !m.Parent.NameLike("Sprite") && - !m.Parent.NameLike("Renderer") && - !m.Parent.NameLike("Scene") && - !m.Parent.NameLike("Authentication") && - !m.Parent.NameLike("GLCore") && - !m.Parent.NameLike("Mdct") && - !m.Parent.NameLike("FormsWindow") && - !m.Parent.NameLike("VorbisCodebook") && - !m.Parent.NameLike("RingBuffer") && - !m.Parent.NameLike("WglGraphicsContext") && - !m.Parent.NameLike("Drawing") && - !m.Parent.NameLike("Win32") && - !m.Parent.NameLike("ICaptureGraphBuilder2") && - - // Don't match a method that overrides a third-party method with many parameters - !m.OverriddensBase.Any(mo => mo.IsThirdParty) && - - // Don't match a constructor that calls a base constructor with many parameters - !(m.IsConstructor && m.MethodsCalled.Any( - mc => mc.IsConstructor && - mc.NbParameters >= 7 && - m.ParentType.DeriveFrom(mc.ParentType))) && - - // Don't match DllImport P/invoke methods with many parameters - !m.HasAttribute("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch()) - - orderby m.NbParameters descending -select new { - m, - m.NbParameters, - Debt = m.NbParameters.Linear(7, 1, 40, 6).ToHours().ToDebt(), - - // The annual interest varies linearly from interest for severity Medium for 7 parameters - // to interest for severity Critical for 40 parameters - AnnualInterest = m.NbParameters.Linear(7, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 40, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest() -} - -// -// This rule matches methods with 7 or more parameters. -// Such method is painful to call and might degrade performance. -// See the definition of the *NbParameters* metric here: -// https://www.ndepend.com/docs/code-metrics#NbParameters -// -// This rule doesn't match a method that overrides a third-party method -// with 7 or more parameters because such situation is the consequence -// of a lower-level problem. -// -// For the same reason, this rule doesn't match a constructor that calls a -// base constructor with 7 or more parameters. -// - -// -// More properties/fields can be added to the declaring type to -// handle numerous states. An alternative is to provide -// a class or a structure dedicated to handle arguments passing. -// For example see the class *System.Diagnostics.ProcessStartInfo* -// and the method *System.Diagnostics.Process.Start(ProcessStartInfo)*. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 1 hour for a method with 7 parameters, -// up to 6 hours for a methods with 40 or more parameters. -//]]> - Avoid methods with too many overloads -// ND1005:AvoidMethodsWithTooManyOverloads -warnif count > 0 -let max = 6 -let lookup = JustMyCode.Methods.Where(m => - m.NbOverloads >= max && - !m.IsOperator && // Don't report operator overload - - // Don't match overloads due tu the visitor pattern, based on a naming convention. - !m.SimpleName.ToLower().StartsWithAny("visit", "dispatch") -).ToLookup(m => m.ParentType.FullName + "."+ m.SimpleName) - -from @group in lookup -let overloads = @group.ToArray() - -// Prune not fixable situations. -let overloadsPruned = overloads.Where( - m => - // Don't match a method that overrides a third-party methods - !m.OverriddensBase.Any(mo => mo.IsThirdParty) && - - // Don't match a constructor that calls a base constructor. - !(m.IsConstructor && m.MethodsCalled.Any( - mc => mc.IsConstructor && - m.ParentType.DeriveFrom(mc.ParentType))) -).ToArray() - -where overloadsPruned.Length > max - -orderby overloads.Length descending - -select new { - m = @group.First(), - overloadsPruned, - Debt = (3*overloads.Length).ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Method overloading is the ability to create multiple methods of the same name -// with different implementations, and various set of parameters. -// -// This rule matches sets of methods with 6 overloads or more. -// -// Such method set might be a problem to maintain -// and provokes coupling higher than necessary. -// -// See the definition of the *NbOverloads* metric here -// https://www.ndepend.com/docs/code-metrics#NbOverloads -// -// Notice that this rule doesn't include in the overloads list -// methods that override a third-party method -// nor constructors that call a base constructor. -// Such situations are consequences of lower-level problems. -// - -// -// Typically the *too many overloads* phenomenon appears when an algorithm -// takes a various set of in-parameters. Each overload is presented as -// a facility to provide a various set of in-parameters. -// In such situation, the C# and VB.NET language feature named -// *Named and Optional arguments* should be used. -// -// The *too many overloads* phenomenon can also be a consequence of the usage -// of the **visitor design pattern** http://en.wikipedia.org/wiki/Visitor_pattern -// since a method named *Visit()* must be provided for each sub type. -// For this reason, the default version of this rule doesn't match overloads whose name -// start with "visit" or "dispatch" (case-unsensitive) to avoid match -// overload visitors, and you can adapt this rule to your own naming convention. -// -// Sometime *too many overloads* phenomenon is not the symptom of a problem, -// for example when a *numeric to something conversion* method applies to -// all numeric and nullable numeric types. -// -// The estimated Debt, which means the effort to fix such issue, -// is of 3 minutes per method overload. -//]]> - Avoid methods potentially poorly commented -// ND1006:AvoidMethodsPotentiallyPoorlyCommented -warnif count > 0 from t in JustMyCode.Types where - // Entity Framework ModelSnapshot and DbContext and Migration have large uncommented methods. - !t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch()) && - !t.DeriveFrom("Microsoft.EntityFrameworkCore.DbContext".AllowNoMatch()) && - !t.DeriveFrom("Microsoft.EntityFrameworkCore.Migrations.Migration".AllowNoMatch()) && - !t.FullName.Contains("NVorbis") && - !t.FullName.Contains("OpenGLDevice") && - !t.FullName.Contains("Matrix") && - !t.FullName.Contains("Shader") && - !t.FullName.Contains("Drawing") && - !t.FullName.Contains("Scene") && - !t.FullName.Contains("Render") && - !t.FullName.Contains("BinaryData") && - !t.FullName.Contains("Component") && - !t.FullName.Contains("Processor") && - !t.FullName.Contains("WglGraphicsContext") && - !t.FullName.Contains("SphereMeshCreator") && - !t.FullName.Contains("WebCam") && - !t.FullName.Contains("DepthFoV") && - !t.FullName.Contains("PixelInvestigation") && - !t.FullName.Contains("DepthDataPostProcessing") - - - - -from m in t.Methods where - m.PercentageComment < 10 && - m.NbLinesOfCode > 20 && - JustMyCode.Contains(t) - - let nbLinesOfCodeNotCommented = m.NbLinesOfCode - m.NbLinesOfComment - - orderby nbLinesOfCodeNotCommented descending - -select new { - m, - m.PercentageComment, - m.NbLinesOfCode, - m.NbLinesOfComment, - nbLinesOfCodeNotCommented, - - Debt = nbLinesOfCodeNotCommented .Linear(20, 2, 200, 20).ToMinutes().ToDebt(), - - // The annual interest varies linearly from interest for severity major for 300 loc - // to interest for severity critical for 2000 loc - AnnualInterest = m.PercentageComment.Linear( - 0, 8 *(Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes), - 20, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest() -} - -// -// This rule matches methods with less than 10% of comment lines and that have -// at least 20 lines of code. Such method might need to be more commented. -// -// See the definitions of the *Comments metric* here: -// https://www.ndepend.com/docs/code-metrics#PercentageComment -// https://www.ndepend.com/docs/code-metrics#NbLinesOfComment -// -// Notice that only comments about the method implementation -// (comments in method body) are taken account. -// - -// -// Typically add more comment. But code commenting is subject to controversy. -// While poorly written and designed code would needs a lot of comment -// to be understood, clean code doesn't need that much comment, especially -// if variables and methods are properly named and convey enough information. -// Unit-Test code can also play the role of code commenting. -// -// However, even when writing clean and well-tested code, one will have -// to write **hacks** at a point, usually to circumvent some API limitations or bugs. -// A hack is a non-trivial piece of code, that doesn't make sense at first glance, -// and that took time and web research to be found. -// In such situation comments must absolutely be used to express the intention, -// the need for the hacks and the source where the solution has been found. -// -// The estimated Debt, which means the effort to comment such method, -// varies linearly from 2 minutes for 10 lines of code not commented, -// up to 20 minutes for 200 or more, lines of code not commented. -//]]> - Avoid types with poor cohesion -// ND1007:AvoidTypesWithPoorCohesion -warnif count > 0 from t in JustMyCode.Types where - t.LCOM > 0.8 && - t.NbFields > 10 && - t.NbMethods >10 && - t.Name != "AppRunner" && - t.Name != "WglGraphicsContext" && - t.Name != "EntityRenderer" && - t.Name != "EngineTypesResolver" && - t.Name != "VorbisStreamDecoder" && - t.Name != "Drawing" && - t.Name != "Matrix" && - t.Name != "BinaryDataConverter" && - t.Name != "SetDataPropertyProcessor" && - t.Name != "Renderable" && - t.Name != "CircularBuffer" && - t.Name != "RegisterScene" && - t.Name != "MainWindow" && - !t.FullName.Contains("Entities") && - t.Name != "AssemblyTypeLoader" - - let poorCohesionScore = 1/(1.01 - t.LCOM) - orderby poorCohesionScore descending - - select new { - t, - t.LCOM, - t.NbMethods, - t.NbFields, - poorCohesionScore, - - Debt = poorCohesionScore.Linear(5, 5, 50, 4*60).ToMinutes().ToDebt(), - - // The annual interest varies linearly from interest for severity Medium for low poorCohesionScore - // to 4 times interest for severity High for high poorCohesionScore - AnnualInterest = poorCohesionScore.Linear(5, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 50, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() - -} - -// -// This rule is based on the *LCOM code metric*, -// LCOM stands for **Lack Of Cohesion of Methods**. -// See the definition of the LCOM metric here -// https://www.ndepend.com/docs/code-metrics#LCOM -// -// The LCOM metric measures the fact that most methods are using most fields. -// A class is considered utterly cohesive (which is good) -// if all its methods use all its instance fields. -// -// Only types with enough methods and fields are taken account to avoid bias. -// The LCOM takes its values in the range [0-1]. -// -// This rule matches types with LCOM higher than 0.8. -// Such value generally pinpoints a **poorly cohesive class**. -// - -// -// To refactor a poorly cohesive type and increase code quality and maintainability, -// certainly you'll have to split the type into several smaller and more cohesive types -// that together, implement the same logic. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 5 minutes for a type with a low poorCohesionScore, -// up to 4 hours for a type with high poorCohesionScore. -//]]> - Avoid methods with too many local variables -// ND1008:AvoidMethodsWithTooManyLocalVariables -warnif count > 0 from m in JustMyCode.Methods where - m.NbVariables > 15 - orderby m.NbVariables descending -select new { - m, - m.NbVariables, - - Debt = m.NbVariables.Linear(15, 1, 80, 6).ToHours().ToDebt(), - - // The annual interest varies linearly from interest for severity Medium for 15 variables - // to interest for severity Critical for 80 variables - AnnualInterest = m.NbVariables.Linear(15, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 80, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest() - -} - -// -// This rule matches methods with more than 15 variables. -// -// Methods where *NbVariables > 8* are hard to understand and maintain. -// Methods where *NbVariables > 15* are extremely complex and must be refactored. -// -// The number of variables is infered from the compiled IL code of the method. -// The C# and VB.NET compiler might introduce some hidden variables -// for language constructs like lambdas, so the default threshold of -// this rule is set to 15 to avoid matching false positives. -// - -// -// To refactor such method and increase code quality and maintainability, -// certainly you'll have to split the method into several smaller methods -// or even create one or several classes to implement the logic. -// -// During this process it is important to question the scope of each -// variable local to the method. This can be an indication if -// such local variable will become an instance field of the newly created class(es). -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 10 minutes for a method with 15 variables, -// up to 2 hours for a methods with 80 or more variables. -//]]> - - - From now, all types added should respect basic quality principles -// ND1100:FromNowAllTypesAddedShouldRespectBasicQualityPrinciples -warnif count > 0 from t in JustMyCode.Types where - -// Only match types added since Baseline. -// Uncomment this line to match also refactored types since Baseline. -// (t.WasAdded() || t.CodeWasChanged()) && - t.WasAdded() && - -// Eliminate interfaces, enumerations or types only with constant fields -// by making sure we are matching type with code. -t.NbLinesOfCode > 10 && - -// Optimization: Fast discard of non-relevant types -(t.Fields.Count() > 20 || t.Methods.Count() > 20) - -// Count instance fields and non-constant static fields -let fields = t.Fields.Where(f => - !f.IsLiteral && - !(f.IsStatic && f.IsInitOnly)) - -// Don't match these methods -let methods = t.Methods.Where( - m => !(// m.IsConstructor || m.IsClassConstructor || // ctor/cctor not enumerated through IType.Methods - m.IsGeneratedByCompiler || - m.IsPropertyGetter || m.IsPropertySetter || - m.IsEventAdder || m.IsEventRemover)) - -where - -// Low Quality types Metrics' definitions are available here: -// https://www.ndepend.com/docs/code-metrics#MetricsOnTypes -( // Types with too many methods - fields.Count() > 20 || - - methods.Count() > 20 || - - // Complex Types that use more than 50 other types - t.NbTypesUsed > 50 -) -select new { - t, - t.NbLinesOfCode, - - instanceMethods = methods.Where(m => !m.IsStatic), - staticMethods = methods.Where(m => m.IsStatic), - - instanceFields = fields.Where(f => !f.IsStatic), - staticFields = fields.Where(f => f.IsStatic), - - t.TypesUsed, - - // Constant Debt estimation, since for such type rules in category "Code Smells" - // accurately estimate the Debt. - Debt = 10.ToMinutes().ToDebt(), - - // The Severity is higher for new types than for refactored types - AnnualInterest= (t.WasAdded() ? 3 : 1) * - Severity.High.AnnualInterestThreshold() -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// This rule operates only on types added since baseline. -// -// This rule can be easily modified to also match types refactored since baseline, -// that don't satisfy all quality criterions. -// -// Types matched by this rule not only have been recently added or refactored, -// but also somehow violate one or several basic quality principles, -// whether it has too many methods, -// it has too many fields, -// or is using too many types. -// Any of these criterions is often a symptom of a type with too many responsibilities. -// -// Notice that to count methods and fields, methods like constructors -// or property and event accessors are not taken account. -// Notice that constants fields and static-readonly fields are not counted. -// Enumerations types are not counted also. -// - -// -// To refactor such type and increase code quality and maintainability, -// certainly you'll have to split the type into several smaller types -// that together, implement the same logic. -// -// Issues of this rule have a constant 10 minutes Debt, because the Debt, -// which means the effort to fix such issue, is already estimated for issues -// of rules in the category **Code Smells**. -// -// However issues of this rule have a **High** severity, with even more -// interests for issues on new types since baseline, because the proper time -// to increase the quality of these types is **now**, before they get commited -// in the next production release. -//]]> - From now, all types added should be 100% covered by tests -// ND1101:FromNowAllTypesAddedShouldBe100PercentCoveredByTests -warnif count > 0 from t in JustMyCode.Types where - -// Only match types added since Baseline. -// Uncomment this line to match also refactored types since Baseline. -// (t.WasAdded() || t.CodeWasChanged()) && - t.WasAdded() && - - // …that are not 100% covered by tests - t.PercentageCoverage < 100 - - let methodsCulprit = t.Methods.Where(m => m.PercentageCoverage < 100) - -select new { - t, - t.PercentageCoverage, - methodsCulprit, - t.NbLinesOfCode, - - // Constant Debt estimation, since for such type rules in category "Coverage" - // accurately estimate the untested code Debt. - Debt = 10.ToMinutes().ToDebt(), - - // The Severity is higher for new types than for refactored types - AnnualInterest= (t.WasAdded() ? 3 : 1) * - Severity.High.AnnualInterestThreshold() -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// This rule operates only on types added since baseline. -// -// This rule can be easily modified to also match types refactored since baseline, -// that are not 100% covered by tests. -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// Often covering 10% of remaining uncovered code of a class, -// requires as much work as covering the first 90%. -// For this reason, typically teams estimate that 90% coverage is enough. -// However *untestable code* usually means *poorly written code* -// which usually leads to *error prone code*. -// So it might be worth refactoring and making sure to cover the 10% remaining code -// because **most tricky bugs might come from this small portion of hard-to-test code**. -// -// Not all classes should be 100% covered by tests (like UI code can be hard to test) -// but you should make sure that most of the logic of your application -// is defined in some *easy-to-test classes*, 100% covered by tests. -// -// In this context, this rule warns when a type added or refactored since the baseline, -// is not fully covered by tests. -// - -// -// Write more unit-tests dedicated to cover code not covered yet. -// If you find some *hard-to-test code*, it is certainly a sign that this code -// is not *well designed* and hence, needs refactoring. -// -// You'll find code impossible to cover by unit-tests, like calls to *MessageBox.Show()*. -// An infrastructure must be defined to be able to *mock* such code at test-time. -// -// Issues of this rule have a constant 10 minutes Debt, because the Debt, -// which means the effort to write tests for the culprit type, is already -// estimated for issues in the category **Code Coverage**. -// -// However issues of this rule have a **High** severity, with even more -// interests for issues on new types since baseline, because the proper time -// to write tests for these types is **now**, before they get commited -// in the next production release. -//]]> - From now, all methods added should respect basic quality principles -// ND1102:FromNowAllMethodsAddedShouldRespectBasicQualityPrinciples -warnif count > 0 from m in JustMyCode.Methods where - - // Only match methods added since Baseline. - // Uncomment this line to match also refactored methods since Baseline. - // (m.WasAdded() || m.CodeWasChanged()) && - m.WasAdded() && - - // Don't match async methods here to avoid - // false positives because of special compiler tricks. - !m.IsAsync && - -// Low Quality methods// Metrics' definitions -( m.NbLinesOfCode > 30 || // https://www.ndepend.com/docs/code-metrics#NbLinesOfCode - m.NbILInstructions > 200 || // https://www.ndepend.com/docs/code-metrics#NbILInstructions - m.CyclomaticComplexity > 20 || // https://www.ndepend.com/docs/code-metrics#CC - m.ILCyclomaticComplexity > 50 || // https://www.ndepend.com/docs/code-metrics#ILCC - m.ILNestingDepth > 4 || // https://www.ndepend.com/docs/code-metrics#ILNestingDepth - m.NbParameters > 5 || // https://www.ndepend.com/docs/code-metrics#NbParameters - m.NbVariables > 8 || // https://www.ndepend.com/docs/code-metrics#NbVariables - m.NbOverloads > 6 ) -select new { - m, - m.NbLinesOfCode, - m.NbILInstructions, - m.CyclomaticComplexity, - m.ILCyclomaticComplexity, - m.ILNestingDepth, - m.NbParameters, - m.NbVariables, - m.NbOverloads, // https://www.ndepend.com/docs/code-metrics#NbOverloads - - // Constant Debt estimation, since for such method rules in category "Code Smells" - // accurately estimate the Debt. - Debt = 5.ToMinutes().ToDebt(), - - // The Severity is higher for new methods than for refactored methods - AnnualInterest= (m.WasAdded() ? 3 : 1) * - Severity.High.AnnualInterestThreshold() -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// This rule operates only on methods added or refactored since the baseline. -// -// This rule can be easily modified to also match methods refactored since baseline, -// that don't satisfy all quality criterions. -// -// Methods matched by this rule not only have been recently added or refactored, -// but also somehow violate one or several basic quality principles, -// whether it is too large (too many *lines of code*), -// too complex (too many *if*, *switch case*, loops…) -// has too many variables, too many parameters -// or has too many overloads. -// - -// -// To refactor such method and increase code quality and maintainability, -// certainly you'll have to split the method into several smaller methods -// or even create one or several classes to implement the logic. -// -// During this process it is important to question the scope of each -// variable local to the method. This can be an indication if -// such local variable will become an instance field of the newly created class(es). -// -// Large *switch…case* structures might be refactored through the help -// of a set of types that implement a common interface, the interface polymorphism -// playing the role of the *switch cases tests*. -// -// Unit Tests can help: write tests for each method before extracting it -// to ensure you don't break functionality. -// -// Issues of this rule have a constant 5 minutes Debt, because the Debt, -// which means the effort to fix such issue, is already estimated for issues -// of rules in the category **Code Smells**. -// -// However issues of this rule have a **High** severity, with even more -// interests for issues on new methods since baseline, because the proper time -// to increase the quality of these methods is **now**, before they get commited -// in the next production release. -//]]> - Avoid decreasing code coverage by tests of types -// ND1103:AvoidDecreasingCodeCoverageByTestsOfTypes -warnif count > 0 -from t in JustMyCode.Types where - t.IsPresentInBothBuilds() && t.CoverageDataAvailable && t.OlderVersion().CoverageDataAvailable -let locDiff = (int)t.NbLinesOfCode.Value - (int)t.OlderVersion().NbLinesOfCode.Value -where locDiff >= 0 -let uncoveredLoc = (int)t.NbLinesOfCodeNotCovered.Value - ((int)t.OlderVersion().NbLinesOfCodeNotCovered.Value + locDiff) -where uncoveredLoc > 0 - -orderby uncoveredLoc descending - -select new { - t, - OldCoveragePercent = t.OlderVersion().PercentageCoverage, - NewCoveragePercent = t.PercentageCoverage, - OldLoc = t.OlderVersion().NbLinesOfCode, - NewLoc = t.NbLinesOfCode, - uncoveredLoc, - - Debt = uncoveredLoc.Linear(1, 15, 100, 3*60).ToMinutes().ToDebt(), - - // The annual interest varies linearly from interest for severity High for one line of code that is not covered by tests anymore - // to interest for severity Critical for 50 lines of code that are not covered by tests anymore - AnnualInterest = uncoveredLoc.Linear(1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes, - 50, 2*Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes).ToMinutes().ToAnnualInterest() - - -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// This rule warns when the number of lines of a type covered by tests -// decreased since the baseline. In case the type faced some refactoring -// since the baseline, this loss in coverage is estimated only for types -// with more lines of code, where # lines of code covered now is lower -// than # lines of code covered in baseline + the extra number of -// lines of code. -// -// Such situation can mean that some tests have been removed -// but more often, this means that the type has been modified, -// and that changes haven't been covered properly by tests. -// -// To visualize changes in code, right-click a matched type and select: -// -// • Compare older and newer versions of source file -// -// • or Compare older and newer versions disassembled with Reflector -// - -// -// Write more unit-tests dedicated to cover changes in matched types -// not covered yet. -// If you find some *hard-to-test code*, it is certainly a sign that this code -// is not *well designed* and hence, needs refactoring. -// -// The estimated Debt, which means the effort to cover by test -// code that used to be covered, varies linearly 15 minutes to 3 hours, -// depending on the number of lines of code that are not covered by tests anymore. -// -// Severity of issues of this rule varies from **High** to **Critical** -// depending on the number of lines of code that are not covered by tests anymore. -// Because the loss in code coverage happened since the baseline, -// the severity is high because it is important to focus on these issues -// **now**, before such code gets released in production. -//]]> - Avoid making complex methods even more complex -// ND1104:AvoidMakingComplexMethodsEvenMoreComplex -warnif count > 0 - -let complexityScoreProc = new Func(m => - (m.CyclomaticComplexity + m.ILCyclomaticComplexity/3 + 5*m.ILNestingDepth).Value) - -from m in JustMyCode.Methods where - - // Don't match async methods here to avoid - // false positives because of special compiler tricks. - !m.IsAsync && - - !m.IsAbstract && - m.IsPresentInBothBuilds() && - m.CodeWasChanged() && - m.OlderVersion().CyclomaticComplexity > 6 - -let complexityScore = complexityScoreProc(m) -let oldComplexityScore = complexityScoreProc(m.OlderVersion()) -where complexityScore > oldComplexityScore - -let complexityScoreDiff = complexityScoreProc(m) - complexityScoreProc(m.OlderVersion()) -orderby complexityScoreDiff descending - -select new { - m, - oldComplexityScore , - complexityScore , - diff= complexityScoreDiff, - - Debt = complexityScoreDiff.Linear(1, 15, 50, 60).ToMinutes().ToDebt(), - - // The annual interest varies linearly from interest for severity Medium for a tiny complexity increment - // to interest for severity critical for 2000 loc - AnnualInterest = complexityScoreDiff.Linear(1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes, - 50, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() - -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// The method complexity is measured through the code metric -// *Cyclomatic Complexity* defined here: -// https://www.ndepend.com/docs/code-metrics#CC -// -// This rule warns when a method already complex -// (i.e with *Cyclomatic Complexity* higher than 6) -// become even more complex since the baseline. -// -// This rule needs assemblies PDB files and source code -// to be available at analysis time, because the *Cyclomatic Complexity* -// is inferred from the source code and source code location -// is inferred from PDB files. See: -// https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation -// -// To visualize changes in code, right-click a matched method and select: -// -// • Compare older and newer versions of source file -// -// • or Compare older and newer versions disassembled with Reflector -// - -// -// A large and complex method should be split in smaller methods, -// or even one or several classes can be created for that. -// -// During this process it is important to question the scope of each -// variable local to the method. This can be an indication if -// such local variable will become an instance field of the newly created class(es). -// -// Large *switch…case* structures might be refactored through the help -// of a set of types that implement a common interface, the interface polymorphism -// playing the role of the *switch cases tests*. -// -// Unit Tests can help: write tests for each method before extracting it -// to ensure you don't break functionality. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 15 to 60 minutes depending on the extra complexity added. -// -// Issues of this rule have a **High** severity, because it is important to focus -// on these issues **now**, before such code gets released in production. -//]]> - Avoid making large methods even larger -// ND1105:AvoidMakingLargeMethodsEvenLarger -warnif count > 0 -from m in JustMyCode.Methods where - !m.IsAbstract && - - // Eliminate constructors from match, since they get larger - // as soons as some fields initialization are added. - !m.IsConstructor && - !m.IsClassConstructor && - - // Filter just here for optimization - m.NbLinesOfCode > 15 && - - m.IsPresentInBothBuilds() && - m.CodeWasChanged() - -let oldLoc = m.OlderVersion().NbLinesOfCode -where oldLoc > 15 && m.NbLinesOfCode > oldLoc - -let diff = m.NbLinesOfCode - oldLoc -where diff > 0 -orderby diff descending - -select new { - m, - oldLoc, - newLoc = m.NbLinesOfCode, - diff, - - Debt = diff.Linear(1, 10, 100, 60).ToMinutes().ToDebt(), - - // The annual interest varies linearly from interest for severity Medium for a tiny complexity increment - // to interest for severity critical for 2000 loc - AnnualInterest = diff .Linear(1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes, - 100, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() - -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule warns when a method already large -// (i.e with more than 15 lines of code) -// become even larger since the baseline. -// -// The method size is measured through the code metric -// *# Lines of Code* defined here: -// https://www.ndepend.com/docs/code-metrics#NbLinesOfCode -// -// This rule needs assemblies PDB files -// to be available at analysis time, because the *# Lines of Code* -// is inferred from PDB files. See: -// https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation -// -// To visualize changes in code, right-click a matched method and select: -// -// • Compare older and newer versions of source file -// -// • or Compare older and newer versions disassembled with Reflector -// - -// -// Usually too big methods should be split in smaller methods. -// -// But long methods with no branch conditions, that typically initialize some data, -// are not necessarily a problem to maintain, and might not need refactoring. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 5 to 20 minutes depending -// on the number of lines of code added. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 10 to 60 minutes depending on the extra complexity added. -// -// Issues of this rule have a **High** severity, because it is important to focus -// on these issues **now**, before such code gets released in production. -//]]> - Avoid adding methods to a type that already had many methods -// ND1106:AvoidAddingMethodsToATypeThatAlreadyHadManyMethods -warnif count > 0 - -// Don't count constructors and methods generated by the compiler! -let getMethodsProc = new Func>( - t => t.Methods.Where(m => - //!m.IsConstructor && !m.IsClassConstructor && // ctor/cctor not enumerated through IType.Methods - !m.IsGeneratedByCompiler).ToArray()) - - -from t in JustMyCode.Types where - - t.NbMethods > 30 && // Just here for optimization - - t.IsPresentInBothBuilds() - - // Optimization: fast discard of non-relevant types - where t.OlderVersion().NbMethods > 30 - - let oldMethods = getMethodsProc(t.OlderVersion()) - where oldMethods.Count > 30 - - let newMethods = getMethodsProc(t) - where newMethods.Count > oldMethods.Count - - let addedMethods = newMethods.Where(m => m.WasAdded()) - let removedMethods = oldMethods.Where(m => m.WasRemoved()) - - orderby addedMethods.Count() descending - -select new { - t, - nbOldMethods = oldMethods.Count, - nbNewMethods = newMethods.Count, - addedMethods, - removedMethods, - - Debt = (10*addedMethods.Count()).ToMinutes().ToDebt(), - AnnualInterest = addedMethods.Count().Linear( - 1, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 100, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// Types where number of methods is greater than 15 -// might be hard to understand and maintain. -// -// This rule lists types that already had more than 15 methods -// at the baseline time, and for which new methods have been added. -// -// Having many methods for a type might be a symptom -// of too many responsibilities implemented. -// -// Notice that constructors and methods generated by the compiler -// are not taken account. -// - -// -// To refactor such type and increase code quality and maintainability, -// certainly you'll have to split the type into several smaller types -// that together, implement the same logic. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 10 minutes per method added. -// -// Issues of this rule have a **High** severity, because it is important to focus -// on these issues **now**, before such code gets released in production. -//]]> - Avoid adding instance fields to a type that already had many instance fields -// ND1107:AvoidAddingInstanceFieldsToATypeThatAlreadyHadManyInstanceFields -warnif count > 0 - -let getFieldsProc = new Func>( - t => t.Fields.Where(f => - !f.IsLiteral && - !f.IsGeneratedByCompiler && - !f.IsStatic).ToArray()) - - -from t in JustMyCode.Types where - - !t.IsEnumeration && - t.IsPresentInBothBuilds() - - // Optimization: fast discard of non-relevant types - where t.OlderVersion().NbFields > 15 - - let oldFields = getFieldsProc(t.OlderVersion()) - where oldFields.Count > 15 - - let newFields = getFieldsProc(t) - where newFields.Count > oldFields.Count - - let addedFields = newFields.Where(f => f.WasAdded()) - let removedFields = oldFields.Where(f => f.WasRemoved()) - - orderby addedFields.Count() descending - -select new { - t, - nbOldFields = oldFields.Count, - nbNewFields = newFields.Count, - addedFields, - removedFields, - - Debt = (10*addedFields.Count()).ToMinutes().ToDebt(), - AnnualInterest = addedFields.Count().Linear( - 1, Severity.High.AnnualInterestThreshold().Value.TotalMinutes, - 100, 4*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)).ToMinutes().ToAnnualInterest() - -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// Types where number of fields is greater than 15 -// might be hard to understand and maintain. -// -// This rule lists types that already had more than 15 fields -// at the baseline time, and for which new fields have been added. -// -// Having many fields for a type might be a symptom -// of too many responsibilities implemented. -// -// Notice that *constants* fields and *static-readonly* fields are not taken account. -// Enumerations types are not taken account also. -// - -// -// To refactor such type and increase code quality and maintainability, -// certainly you'll have to group subsets of fields into smaller types -// and dispatch the logic implemented into the methods -// into these smaller types. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 10 minutes per field added. -// -// Issues of this rule have a **High** severity, because it is important to focus -// on these issues **now**, before such code gets released in production. -//]]> - Avoid transforming an immutable type into a mutable one -// ND1108:AvoidTransformingAnImmutableTypeIntoAMutableOne -warnif count > 0 -from t in Application.Types where - t.CodeWasChanged() && - t.OlderVersion().IsImmutable && - !t.IsImmutable && - // Don't take account of immutable types transformed into static types (not deemed as immutable) - !t.IsStatic - -let culpritFields = t.InstanceFields.Where(f => !f.IsImmutable) -select new { - t, - culpritFields, - Debt = (10 + 10*culpritFields.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// A type is considered as *immutable* if its instance fields -// cannot be modified once an instance has been built by a constructor. -// -// Being immutable has several fortunate consequences for a type. -// For example its instance objects can be used concurrently -// from several threads without the need to synchronize accesses. -// -// Hence users of such type often rely on the fact that the type is immutable. -// If an immutable type becomes mutable, there are chances that this will break -// users code. -// -// This is why this rule warns about such immutable type that become mutable. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 2 minutes per instance field that became mutable. -// - -// -// If being immutable is an important property for a matched type, -// then the code must be refactored to preserve immutability. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 10 minutes plus 10 minutes per instance fields of -// the matched type that is now mutable. -// -// Issues of this rule have a **High** severity, because it is important to focus -// on these issues **now**, before such code gets released in production. -//]]> - - - Avoid interfaces too big -// ND1200:AvoidInterfacesTooBig -warnif count > 0 - -from i in JustMyCode.Types -where i.IsInterface && i.NbMethods >= 10 && // Optimization First threshold - i.Name != "SystemInformation" && - i.Name != "EntityRepository" && - i.Name != "RenderDataArray" && - i.Name != "Graph" && - i.Name != "GraphEngine" && - !i.FullName.Contains("NVorbis") && - !i.FullName.Contains("WebCam") - -// A get;set; property count as one method -let properties = i.Methods.Where(m => m.SimpleName.Length > 4 && (m.IsPropertyGetter || m.IsPropertySetter)) - .Distinct(m => m.SimpleName.Substring(4, m.SimpleName.Length -4)) - -// An event count as one method -let events = i.Methods.Where(m => (m.IsEventAdder|| m.IsEventRemover)) - .Distinct(m => m.SimpleName.Replace("add_","").Replace("remove_","")) - -let methods = i.Methods.Where(m => !m.IsPropertyGetter && !m.IsPropertySetter && !m.IsEventAdder && !m.IsEventRemover) -let methodsCount = methods.Count() + properties.Count() + events.Count() -where methodsCount >= 10 -let publicFactor = i.IsPubliclyVisible ? 1 : 0.5 -orderby methodsCount descending -select new { - i, - Methods= methods, - Properties = properties, - Events = events, - Debt = (publicFactor*methodsCount.Linear(10, 20, 100, 7*60)).ToMinutes().ToDebt(), - // The annual interest varies linearly from interest for severity Medium for an interface with 10 methods - // to interest for severity Critical for an interface with 100 methods and more - AnnualInterest = (publicFactor*methodsCount.Linear( - 10, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 100, Severity.Critical.AnnualInterestThreshold().Value.TotalMinutes)) - .ToMinutes().ToAnnualInterest() -} - - -// -// This rule matches interfaces with more than 10 methods. -// Interfaces are abstractions and are meant to simplify the code structure. -// An interface should represent a single responsibility. -// Making an interface too large, too complex, necessarily means -// that the interface has too many responsibilities. -// -// A property with getter or setter or both count as one method. -// An event count as one method. -// - -// -// Typically to fix such issue, the interface must be refactored -// in a grape of smaller *single-responsibility* interfaces. -// -// A classic example is a *ISession* large interface, responsible -// for holding states, run commands and offer various accesses -// and facilities. -// -// The classic problem for a large public interface is that it has -// many clients that consume it. As a consequence splitting it in -// smaller interfaces has an important impact and it is not always -// feasible. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from 20 minutes for an interface with 10 methods, -// up to 7 hours for an interface with 100 or more methods. -// The Debt is divided by two if the interface is not publicly -// visible, because in such situation only the current project is impacted -// by the refactoring. -// -]]> - Base class should not use derivatives -// ND1201:BaseClassShouldNotUseDerivatives -warnif count > 0 -from baseClass in JustMyCode.Types -where baseClass.IsClass && - baseClass.Name != "Randomizer" && - !baseClass.FullName.Contains("Context") && - baseClass.NbChildren > 0 // <-- for optimization! -let derivedClassesUsed = baseClass.DerivedTypes.UsedBy(baseClass) - // Don't warn when a base class is using nested private derived class - .Where(derivedClass => - !(derivedClass.IsNested && - derivedClass.Visibility == Visibility.Private && - derivedClass.ParentType == baseClass)) -where derivedClassesUsed.Count() > 0 - -let derivedClassesMemberUsed = derivedClassesUsed.SelectMany(c => c.Members).UsedBy(baseClass) -orderby derivedClassesMemberUsed.Count() descending - -select new { - baseClass, - derivedClassesUsed, - derivedClassesMemberUsed, - - Debt = 3*(derivedClassesUsed.Count()+derivedClassesMemberUsed.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// In *Object-Oriented Programming*, the **open/closed principle** states: -// *software entities (components, classes, methods, etc.) should be open -// for extension, but closed for modification*. -// http://en.wikipedia.org/wiki/Open/closed_principle -// -// Hence a base class should be designed properly to make it easy to derive from, -// this is *extension*. But creating a new derived class, or modifying an -// existing one, shouldn't provoke any *modification* in the base class. -// And if a base class is using some derivative classes somehow, there -// are good chances that such *modification* will be needed. -// -// Extending the base class is not anymore a simple operation, -// this is not good design. -// -// Note that this rule doesn't warn when a base class is using a derived class -// that is nested in the base class and declared as private. In such situation -// we consider that the derived class is an encapsulated implementation -// detail of the base class. -// - -// -// Understand the need for using derivatives, -// then imagine a new design, and then refactor. -// -// Typically an algorithm in the base class needs to access something -// from derived classes. You can try to encapsulate this access behind -// an abstract or a virtual method. -// -// If you see in the base class some conditions on *typeof(DerivedClass)* -// not only *urgent refactoring* is needed. Such condition can easily -// be replaced through an abstract or a virtual method. -// -// Sometime you'll see a base class that creates instance of some derived classes. -// In such situation, certainly using the *factory method pattern* -// http://en.wikipedia.org/wiki/Factory_method_pattern -// or the *abstract factory pattern* -// http://en.wikipedia.org/wiki/Abstract_factory_pattern -// will improve the design. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 3 minutes per derived class used by the base class + -// 3 minutes per member of a derived class used by the base class. -//]]> - Class shouldn't be too deep in inheritance tree -// ND1202:ClassShouldntBeTooDeepInInheritanceTree -warnif count > 0 from t in JustMyCode.Types -where t.IsClass && t.BaseClass != null && - !t.Name.Contains("Entity") && - !t.BaseClass.Name.Contains("Entity") && - (t.BaseClass.BaseClass == null || !t.BaseClass.BaseClass.Name.Contains("Entity")) && - (t.BaseClass.BaseClass == null || t.BaseClass.BaseClass.BaseClass == null || !t.BaseClass.BaseClass.BaseClass.Name.Contains("Entity")) && - !t.Name.Contains("Instance") && - !t.Name.Contains("Processor") && - !t.Name.Contains("Renderer") && - !t.Name.Contains("Updater") && - !t.Name.Contains("Resolver") && - !t.Name.Contains("Runner") && - !t.Name.Contains("Tests") && - !t.Name.EndsWith("Shader") && - !t.Name.EndsWith("MusicFile") && - !t.Name.EndsWith("VideoFile") && - !t.Name.StartsWith("Mock") && - !t.Name.StartsWith("Windows") && - !t.Name.EndsWith("raph") && - !t.ParentNamespace.Name.Contains("Messages") && - !t.ParentNamespace.Name.Contains("Networking") -let baseClasses = t.BaseClasses.ExceptThirdParty() -where baseClasses.Count() >= 3 -orderby baseClasses.Count() descending - -select new { - t, - baseClasses, - // The metric value DepthOfInheritance takes account - // of third-party base classessee its definition here: - // https://www.ndepend.com/docs/code-metrics#DIT - t.DepthOfInheritance, - Debt = (baseClasses.Count() -2)*3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about classes having 3 or more base classes. -// Notice that third-party base classes are not counted -// because this rule is about your code design, not -// third-party libraries consumed design. -// -// *In theory*, there is nothing wrong having a *long inheritance chain*, -// if the modelization has been well thought out, -// if each base class is a well-designed refinement of the domain. -// -// *In practice*, modeling properly a domain demands a lot of effort -// and experience and more often than not, a *long inheritance chain* -// is a sign of confused design, that will be hard to work with and maintain. -// - -// -// In *Object-Oriented Programming*, a well-known motto is -// **Favor Composition over Inheritance**. -// -// This is because *inheritance* comes with pitfalls. -// In general, the implementation of a derived class is very bound up with -// the base class implementation. Also a base class exposes implementation -// details to its derived classes, that's why it's often said that -// inheritance breaks encapsulation. -// -// On the other hands, *Composition* favors binding with interfaces -// over binding with implementations. Hence, not only the encapsulation -// is preserved, but the design is clearer, because interfaces make it explicit -// and less coupled. -// -// Hence, to break a *long inheritance chain*, *Composition* is often -// a powerful way to enhance the design of the refactored underlying logic. -// -// You can also read: -// http://en.wikipedia.org/wiki/Composition_over_inheritance and -// http://stackoverflow.com/questions/49002/prefer-composition-over-inheritance -// -// The estimated Debt, which means the effort to fix such issue, -// depends linearly upon the depth of inheritance. -//]]> - Class with no descendant should be sealed if possible -// ND1203:ClassWithNoDescendantShouldBeSealedIfPossible -warnif count > 0 from t in JustMyCode.Types where - t.IsClass && - t.NbChildren ==0 && - !t.Name.StartsWith("Scene") && - !t.Name.StartsWith("AssemblyTypeLoader") && - !t.Name.StartsWith("SharedComponentNodes") && - !t.Name.StartsWith("SharedNodeList") && - !t.Name.StartsWith("WglGraphicsContext") && - !t.Name.StartsWith("MsAdpcmConverter") && - !t.Name.StartsWith("ComponentTypesComparer") && - !t.Name.StartsWith("MethodLoader") && - !t.Name.StartsWith("CachedNodesAndRenderersToAddAndRemove") && - !t.Name.StartsWith("WindowsDisplayDevice") && - !t.Name.Contains("Tests") && - !t.Name.StartsWith("NullContext") && - !t.Name.StartsWith("TestHub") && - - (t.BaseClass == null || t.BaseClass.Name != "Exception") && - (t.ParentType == null || !t.ParentType.IsInternal) && - t.ParentType == null && - !t.IsSealed && - !t.IsStatic && - t.NbLinesOfCode != 0 && - !t.IsPubliclyVisible // You might want to comment this condition - // if you are developing an application, - // instead of developing a library - // with public classes that are intended to be - // sub-classed by your clients. -&& !t.FullName.StartsWith("NVorbis") - orderby t.NbLinesOfCode descending -select new { - t, - t.NbLinesOfCode, - Debt = 30.ToSeconds().ToDebt(), - Severity = Severity.Medium -} - -// -// If a *non-static* class isn't declared with the keyword *sealed*, -// it means that it can be subclassed everywhere the *non-sealed* -// class is visible. -// -// Making a class a *base class* requires significant design effort. -// Subclassing a *non-sealed* class, not initially designed -// to be subclassed, will lead to unanticipated design issue. -// -// Most classes are *non-sealed* because developers don't care about -// the keyword *sealed*, not because the primary intention was to write -// a class that can be subclassed. -// -// There are minor performance gain in declaring a class as *sealed*. -// But the real benefit of doing so, is actually to **express the -// intention**: *this class has not be designed to be a base class, -// hence it is not allowed to subclass it*. -// -// Notice that by default this rule doesn't match *public* class -// to avoid matching classes that are intended to be sub-classed by -// third-party code using your library. -// If you are developing an application and not a library, -// just uncomment the clause *!t.IsPubliclyVisible*. -// - -// -// For each matched class, take the time to assess if it is really -// meant to be subclassed. Certainly most matched class will end up -// being declared as *sealed*. -//]]> - Overrides of Method() should call base.Method() -// ND1204:OverridesOfMethodShouldCallBaseMethod -warnif count > 0 -from t in Types // Take account of third-party base classes also - -// Bother only classes with descendant -where t.IsClass && t.NbChildren > 0 - -from mBase in t.InstanceMethods -where mBase.IsVirtual && - !mBase.IsThirdParty && - !mBase.IsAbstract && - !mBase.IsExplicitInterfaceImpl && - !mBase.IsPropertyGetter && - !mBase.IsPropertySetter && - !mBase.IsIndexerGetter && - !mBase.IsIndexerSetter && - !mBase.Name.StartsWith("Equals(") && - // Don't take account of virtual methods of the Object type. - !mBase.Name.EqualsAny("Equals(Object)","ToString()", "GetHashCode()","Finalize()", "Get(String)", "Dispose()") - -from mOverride in mBase.OverridesDirectDerived -where !mOverride.IsUsing(mBase) && - !mOverride.FullName.Contains("Mock") && - !mOverride.FullName.Contains("Online") && - mOverride.Name != "ToString()" && - !mOverride.Name.StartsWith("On") && - !mOverride.Name.StartsWith("AddVertices(") && - !mOverride.Name.StartsWith("FillVertices") && - !mOverride.Name.StartsWith("GetEdge") && - !mOverride.Name.Contains("FindType") && - mOverride.Name != "RunOneTick()" && - mOverride.Name != "LoadContentSettings()" && - !mOverride.FullName.Contains("Reloadable") && - !mOverride.FullName.Contains("NVorbis") && - !mOverride.ParentType.Name.Contains("VertexPair") && - JustMyCode.Contains(mOverride) // Don't warn on generated code -select new { - mOverride, - shouldCall = mBase, - definedInBaseClass = mBase.ParentType, - - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Typically overrides of a base method, should **refine** or **complete** -// the behavior of the base method. If the base method is not called, -// the base behavior is not refined but it is *replaced*. -// -// Violations of this rule are a sign of *design flaw*, -// especially if the actual design provides valid reasons -// that advocates that the base behavior must be replaced and not refined. -// - -// -// You should investigate if *inheritance* is the right choice -// to bind the base class implementation with the derived classes -// implementations. Does presenting the method with polymorphic -// behavior through an interface, would be a better design choice? -// -// In such situation, often using the design pattern **template method** -// http://en.wikipedia.org/wiki/Template_method_pattern might help -// improving the design. -//]]> - Do not hide base class methods -// ND1205:DoNotHideBaseClassMethods -warnif count > 0 - -// Define a lookup table indexing methods by their name including parameters signature. -let lookup = Methods.Where(m => !m.IsConstructor && !m.IsStatic && !m.IsGeneratedByCompiler) - .ToLookup(m1 => m1.Name) - -from t in Application.Types -where !t.IsStatic && t.IsClass && - // Discard classes deriving directly from System.Object - t.DepthOfInheritance > 1 -where t.BaseClasses.Any() - -// For each methods not overriding any methods (new slot), -// let's check if it hides by name some methods defined in base classes. -from m in t.InstanceMethods -where m.IsNewSlot && !m.IsExplicitInterfaceImpl && !m.IsGeneratedByCompiler - -// Notice how lookup is used to quickly retrieve methods with same name as m. -// This makes the query 10 times faster than iterating each base methods to check their name. -let baseMethodsHidden = lookup[m.Name].Where(m1 => m1 != m && - !m1.IsAbstract && - t.DeriveFrom(m1.ParentType)) - -where baseMethodsHidden.Count() > 0 -select new { - m, - baseMethodsHidden, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// Method hiding is when a base class has a non-virtual method *M()*, -// and a derived class has also a method *M()* with the same signature. -// In such situation, calling *base.M()* does something different -// than calling *derived.M()*. -// -// Notice that this is not *polymorphic* behavior. With *polymorphic* -// behavior, calling both *base.M()* and *derived.M()* on an instance -// object of *derived*, invoke the same implementation. -// -// This situation should be avoided because it obviously leads to confusion. -// This rule warns about all method hiding cases in the code base. -// - -// -// To fix a violation of this rule, remove or rename the method, -// or change the parameter signature so that the method does -// not hide the base method. -// -// However *method hiding is for those times when you need to have two -// things to have the same name but different behavior*. This is a very -// rare situations, described here: -// http://blogs.msdn.com/b/ericlippert/archive/2008/05/21/method-hiding-apologia.aspx -//]]> - A stateless class or structure might be turned into a static type -// ND1206:AStatelessClassOrStructureMightBeTurnedIntoAStaticType -warnif count > 0 - -let testAttributes = ThirdParty.Types - .Where(t => t.IsAttributeClass && t.SimpleName.Contains("Test")).ToArray() - -from t in JustMyCode.Types where - !t.IsStatic && - !t.IsGeneric && - t.InstanceFields.Count() == 0 && - t.SimpleName != "Program" && // Don't warn on Program classes generated by designers - - // Don't match: - // --> types that implement some interfaces. - t.NbInterfacesImplemented == 0 && - - // --> or classes that have sub-classes children. - t.NbChildren == 0 && - - !t.ParentNamespace.Name.Contains(".Tests") && - !t.ParentNamespace.Name.Contains(".Mocks") && - !t.ParentNamespace.Name.Contains(".Components") && - t.Name != "EntityInstances" && - - // --> or classes which are just DllImport containers - !t.Name.StartsWith("Native") && - - // --> or classes that have a base class - ((t.IsClass && t.DepthOfDeriveFrom("System.Object".AllowNoMatch()) == 1) || - t.IsStructure) && - - // Don't match test classes - !testAttributes.Any(tAttr => t.HasAttribute(tAttr)) - -let methodsUsingMe = t.TypesUsingMe.ChildMethods().Where(m => m.IsUsing(t)) - -select new { - t, - methodsUsingMe, - Debt = (1 + methodsUsingMe.Count()).ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This rule matches classes and structures that are not static, nor generic, -// that doesn't have any instance fields, that doesn't implement any interface -// nor has a base class (different than *System.Object*). -// -// Such class or structure is a *stateless* collection of *pure* functions, -// that doesn't act on any *this* object data. Such collection of *pure* functions -// is better hosted in a **static class**. Doing so simplifies the client code -// that doesn't have to create an object anymore to invoke the *pure* functions. -// - -// -// Declare all methods as *static* and transform the class or structure -// into a *static* class. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - Non-static classes should be instantiated or turned to static -// ND1207:NonStaticClassesShouldBeInstantiatedOrTurnedToStatic -warnif count > 0 - -let testAttributes = Types - .Where(t => t.IsAttributeClass && t.SimpleName.Contains("Test")).ToArray() - -from t in JustMyCode.Types -where t.IsClass && - !t.IsPublic && // if you are developing a framework, - // you might not want to match public classes - !t.FullName.Contains("NVorbis.Ogg") && - !t.FullName.Contains("Tests") && - !t.Name.StartsWith("Invalid") && - !t.IsSealed && - !t.IsStatic && - !t.IsAbstract && - !t.IsAttributeClass && // Attributes class are never seen as instantiated - - // Don't suggest to turn to static, classes that implement interfaces - t.InterfacesImplemented.Count() == 0 && - - // Find the first constructor of t called - // match t if none of its constructors is called. - t.Constructors.FirstOrDefault(ctor => ctor.NbMethodsCallingMe > 0) == null && - - // Don't warn on Program classes generated by designers - t.SimpleName != "Program" && - - // Types instantiated through remoting infrastructure - !t.DeriveFrom("System.MarshalByRefObject".AllowNoMatch()) && - - // JSON and XML serialized types might not be seen as instantiated. - !t.IsUsing("Newtonsoft.Json".MatchNamespace().AllowNoMatch()) && - !t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) && - !t.IsUsing("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) && - !t.IsUsing("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) && - - // Serialized type might never be seen as instantiated. - !t.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) && - !t.IsUsing("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) && - - // ASP.NET Core ViewModel and Repository - !t.SimpleName.EndsWithAny("Model","Repository") && - !t.IsUsing("System.ComponentModel.DataAnnotations".AllowNoMatch().MatchNamespace()) && - - // ASP.NET Classes that are instantiated by the ASP.NET infrastructure. - !t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWithAny("System.Web", "Microsoft.AspNetCore")) && - !(t.Constructors.Count() == 1 && t.Constructors.Single().Name.Contains("(IHostingEnvironment)")) && - - // Entity Framework ModelSnapshot and DbContext and Migration - !t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch()) && - !t.DeriveFrom("Microsoft.EntityFrameworkCore.DbContext".AllowNoMatch()) && - !t.DeriveFrom("Microsoft.EntityFrameworkCore.Migrations.Migration".AllowNoMatch()) && - - // Don't match test classes - !testAttributes.Any(tAttr => t.HasAttribute(tAttr)) && - - // Validtor classes might not be instantiated - !t.SimpleName.EndsWith("Validator") && - - // Don't warn about classes instantiated by Dependency Injection frameworks - !t.TypesUsingMe.Any(t1 => t1.TypesUsed.ParentNamespaces().Any(n => n.Name.StartsWithAny( - "Microsoft.Extensions.DependencyInjection", - "Autofac", "Microsoft.Practices.Unity", "Ninject", - "StructureMap", "SimpleInjector", "Castle.Windsor", - "LightInject", "Spring", "Lamar" - ))) - -select new { - t, - t.Visibility, - Debt = 2.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// Notice that classes only instantiated through reflection, like plug-in root classes -// are matched by this rules. - -// -// If the constructors of a class are never called, the class is -// never instantiated, and should be defined as a *static class*. -// -// However this rule doesn't match instantiation through reflection. -// As a consequence, plug-in root classes, instantiated through reflection -// via *IoC frameworks*, can be *false positives* for this rule. -// -// This rule doesn't match also classes instantiated by the ASP.NET -// infrastructure, ASP.NET view model classes -// and Entity Framework ModelSnapshot, DbContext and Migration classes. -// -// Notice that by default this rule matches also *public* class. -// If you are developing a framework with classes that are intended -// to be instantiated by your clients, just uncomment the line -// *!t.IsPublic*. -// - -// -// First it is important to investigate why the class is never instantiated. -// If the reason is *the class hosts only static methods* then the class -// can be safely declared as *static*. -// -// Others reasons like, *the class is meant to be instantiated via reflection*, -// or *is meant to be instantiated only by client code* should lead to -// adapt this rule code to avoid these matches. -// -]]> - Methods should be declared static if possible -// ND1208:MethodsShouldBeDeclaredStaticIfPossible -warnif count > 0 - -let testAttributes = ThirdParty.Types - .Where(t => t.IsAttributeClass && - ( t.SimpleName.Contains("Test") || - t.SimpleName.Contains("Fact") || - t.SimpleName.Contains("SetUp") || - t.SimpleName.Contains("TearDown") ) - ).ToArray() - -from t in JustMyCode.Types.Where(t => - !t.IsStatic && !t.IsInterface && - !t.IsEnumeration && !t.IsDelegate && - !t.Name.StartsWith("Test") && - !t.Name.Contains("Tests") && - !t.Name.Contains("Mock") && - t.Name != "XmlConversion" && - t.Name != "RegisterScene" && - !t.IsGeneratedByCompiler && - !t.IsRecord && - // Sub types are also broken in NDepend and don't report if they are records - !t.Name.Contains("+") && - - // Don't advise to declare Global ASP.NET or ApiController methods as static - !t.DeriveFrom("System.Web.HttpApplication".AllowNoMatch()) && - !t.DeriveFrom("System.Web.Http.ApiController".AllowNoMatch()) && - !t.DeriveFrom("Microsoft.AspNetCore.Mvc.Controller".AllowNoMatch()) && - - // Don't set as static methods of JSON serialized type - !t.TypesUsed.Any(t1 => t1.IsAttributeClass && t1.ParentNamespace.Name == "Newtonsoft.Json") && - - // Don't set as static methods of XML serialized type - !t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) && - !t.IsUsing("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) && - !t.IsUsing("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch())) - -let methodsThatCanBeMadeStatic = - from m in t.InstanceMethods - - // An instance method can be turned to static if it is not virtual, - // not using the this reference and also, not using - // any of its class or base classes instance fields or instance methods. - where !m.IsAbstract && !m.IsVirtual && - // We don't want public static methods, so allow if public instance method is used - !m.IsPublic && !m.IsInternal && !m.Name.StartsWith("PrivateMethod") && - !m.AccessThis && !m.IsExplicitInterfaceImpl && - !m.IsProtected && // Protected method access doesn't match well with static methods - // Don't warn about not yet implemented methods. - !m.CreateA("System.NotImplementedException".AllowNoMatch()) && - - // Optimization: Using FirstOrDefault() avoid to check all members, - // as soon as one member is found - // we know the method m cannot be made static. - m.MembersUsed.FirstOrDefault( - mUsed => !mUsed.IsStatic && - (mUsed.ParentType == t || - t.DeriveFrom(mUsed.ParentType)) - ) == null - - // Don't match test methods - && !testAttributes.Any(tAttr => m.HasAttribute(tAttr)) - select m - -from m in methodsThatCanBeMadeStatic -let staticFieldsUsed = m.ParentType.StaticFields.UsedBy(m).Where(f => !f.IsGeneratedByCompiler) -let methodsCallingMe = m.MethodsCallingMe - -// All callers of the method must be in JustMyCode, -// else having a method declared as static would break the call from the code generated -// like when a WPF Connect() method is binding a method to an event. -where methodsCallingMe.All(m1 => JustMyCode.Contains(m1)) - -select new { - m, - staticFieldsUsed, - methodsCallingMe, - Debt = (1 + methodsCallingMe.Count())*30.ToSeconds().ToDebt(), - Severity = Severity.Medium -} - -// -// When an instance method can be *safely* declared as static you should declare it as static. -// -// Whenever you write a method, you fulfill a contract in a given scope. -// The narrower the scope is, the smaller the chance is that you write a bug. -// -// When a method is static, you can't access non-static members; hence, your scope is -// narrower. So, if you don't need and will never need (even in subclasses) instance -// fields to fulfill your contract, why give access to these fields to your method? -// Declaring the method static in this case will let the compiler check that you -// don't use members that you do not intend to use. -// -// Declaring a method as static if possible is also good practice because clients can -// tell from the method signature that calling the method can't alter the object's state. -// -// Doing so, is also a micro performance optimization, since a static method is a -// bit cheaper to invoke than an instance method, because the *this* reference* -// doesn't need anymore to be passed. -// -// Notice that if a matched method is a handler, bound to an event through code -// generated by a designer, declaring it as static might break the designer -// generated code, if the generated code use the *this* invocation syntax, -// (like *this.Method()*). -// - -// -// Declare matched methods as static. -// -// Since such method doesn't use any instance fields and methods of its type and -// base-types, you should consider if it makes sense, to move such a method -// to a static utility class. -//]]> - Constructor should not call a virtual method -// ND1209:ConstructorShouldNotCallAVirtualMethod -warnif count > 0 - -from t in JustMyCode.Types where - t.IsClass && - !t.IsGeneratedByCompiler && - !t.IsSealed && - t.Name != "Range" && - t.Name != "AchievementProvider" && - t.Name != "Device" && - t.Name != "FormsWindow" && - t.Name != "ClientAuthenticatorConnection" && - t.Name != "ReloadableWindow" && - t.Name != "XmlFileSettingsStore" && -t.Name != "BufferedReadStream" && - (t.BaseClass == null || - !t.BaseClass.Name.Contains("Exception")) - -from ctor in t.Constructors -let virtualMethodsCalled = - from mCalled in ctor.MethodsCalled - where mCalled.IsVirtual && !mCalled.IsFinal && -//sealed check (IsFinal) does not work well, we gotta ignore manually - !mCalled.Name.StartsWith("TryLoadData") && - !mCalled.Name.StartsWith("SetViewport") && - // Only take care of just-my-code virtual methods called - JustMyCode.Contains(mCalled) && - ( mCalled.ParentType == t || - (t.DeriveFrom(mCalled.ParentType) && - // Don't accept Object methods since they can be called - // from another reference than the 'this' reference. - mCalled.ParentType.FullName != "System.Object") - ) - select mCalled -where virtualMethodsCalled.Count() > 0 - -select new { - ctor , - virtualMethodsCalled, - // If there is no derived type, it might be - // an opportunity to mark t as sealed. - t.DerivedTypes, - Debt = ((virtualMethodsCalled.Count())*6).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule matches constructors of a non-sealed class that call one or -// several virtual methods. -// -// When an object written in C# is constructed, what happens is that constructors -// run in order from the base class to the most derived class. -// -// Also objects do not change type as they are constructed, but start out as -// the most derived type, with the method table being for the most derived type. -// This means that virtual method calls always run on the most derived type, -// even when calls are made from the constructor. -// -// When you combine these two facts you are left with the problem that if you -// make a virtual method call in a constructor, and it is not the most derived -// type in its inheritance hierarchy, then it will be called on a class whose -// constructor has not been run, and therefore may not be in a suitable state -// to have that method called. -// -// Hence this situation makes the class *fragile to derive from*. -// - -// -// Violations reported can be solved by re-designing object initialisation -// or by declaring the parent class as *sealed*, if possible. -// -]]> - Avoid the Singleton pattern -// ND1210:AvoidTheSingletonPattern -warnif count > 0 -from t in Application.Types -where !t.IsStatic && !t.IsAbstract && (t.IsClass || t.IsStructure) - -// All ctors of a singleton are private -where t.Constructors.Where(ctor => !ctor.IsPrivate).Count() == 0 && -!t.FullName.Contains("Component") && -!t.FullName.Contains("Message") && -!t.FullName.Contains(".Tests") - -// Require mutable instance fields to be shared across the several clients of the single object. -let mutableInstanceFields = t.InstanceFields.Where(f => !f.IsImmutable) -where mutableInstanceFields.Any() - -// A singleton contains one or several static fields of its parent type, -// or of an interface implented by its parent type, -// to reference the unique instance -let staticFieldInstances = t.StaticFields.WithFieldTypeIn(t.InterfacesImplemented.Concat(t)) -where staticFieldInstances.Count() == 1 - -let staticFieldInstance = staticFieldInstances.Single() -let methodsUsingField = staticFieldInstance.MethodsUsingMe -let methodsUsingField2 = methodsUsingField.Concat(methodsUsingField.SelectMany(m => m.MethodsCallingMe)) - -select new { - t, - staticFieldInstance, - methodsUsingField2, - mutableInstanceFields , - Debt = (3*methodsUsingField2.Count()).ToMinutes().ToDebt(), - AnnualInterest = (10+methodsUsingField2.Count()).ToMinutes().ToAnnualInterest() -} - -// -// The *singleton pattern* consists in enforcing that a class has just -// a single instance: http://en.wikipedia.org/wiki/Singleton_pattern -// At first glance, this pattern looks appealing, it is simple to implement, -// it adresses a common situation, and as a consequence it is widely used. -// -// However, we discourage you from using singleton classes because experience -// shows that **singleton often results in less testable and less maintainable code**. -// Singleton is *by-design*, not testable. Each unit test should use their own objects -// while singleton forces multiple unit-tests to use the same instance object. -// -// Also the singleton static *GetInstance()* method allows *magic* access to that -// single object and its state from wherever developers want! This potentially -// attractive facility unfortunatly ends up into *unorganized*/*messy* code that -// will require effort to be refactored. -// -// Notice that this rule matches only singleton types with **mutable** instance fields -// because singleton pitfalls result from anarchical access and modification -// of instance data. -// -// More details available in these discussions: -// http://codebetter.com/patricksmacchia/2011/05/04/back-to-basics-usage-of-static-members/ -// http://adamschepis.com/blog/2011/05/02/im-adam-and-im-a-recovering-singleton-addict/ -// - -// -// This rule matches *the classic syntax of singletons*, where one -// static field hold the single instance of the parent class. We underline that -// *the problem is this particular syntax*, that plays against testability. -// The problem is not the fact that a single instance of the class lives -// at runtime. -// -// Hence to fix matches fo this rule, creates the single instance -// at the startup of the program, and pass it to all classes and methods -// that need to access it. -// -// If multiple singletons are identified, they actually form together a -// *program execution context*. Such context can be unified in a unique -// singleton context. Doing so will make it easier to propagate the -// context across the various program units. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 3 minutes per method relying on the singleton. -// It is not rare that hundreds of methods rely on the singleton -// and that it takes hours to get rid of a singleton, refactoring -// the way just explained above. -// -// The severity of each singleton issue is **Critical** because as -// explained, using a the singleton pattern can really prevent the -// whole program to be testable. -//]]> - Don't assign static fields from instance methods -// ND1211:DontAssignStaticFieldsFromInstanceMethods -warnif count > 0 -from f in Application.Fields where - f.IsStatic && - !f.IsLiteral && - !f.IsInitOnly && - !f.IsGeneratedByCompiler && - // Contract API define such a insideContractEvaluation static field - f.Name != "insideContractEvaluation" && - f.Name != "currentSceneInAllThreads" && - f.Name != "registerInAllThreadsWhenNotStartedFromTest" && - f.Name != "CheckShaderLogWhenInitializing" && - f.Name != "autoIncreasingServerPortToPreventConflicts" && - f.Name != "engineTypes" && - f.Name != "thisFrameTicks" && - f.Name != "random" && -f.Name!="Current" && - !f.FullName.Contains("Mock") && - !f.Name.Contains("cache") -let assignedBy = f.MethodsAssigningMe.Where(m => !m.IsStatic) -where assignedBy .Count() > 0 -select new { - f, - assignedBy, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Assigning static fields from instance methods leads to -// poorly maintainable and non-thread-safe code. -// -// More discussion on the topic can be found here: -// http://codebetter.com/patricksmacchia/2011/05/04/back-to-basics-usage-of-static-members/ -// - -// -// If the *static* field is just assigned once in the program -// lifetime, make sure to declare it as *readonly* and assign -// it inline, or from the static constructor. -// -// In *Object-Oriented-Programming* the natural artifact -// to hold states that can be modified is **instance fields**. -// -// Hence to fix violations of this rule, make sure to -// hold assignable states through *instance* fields, not -// through *static* fields. -//]]> - Avoid empty interfaces -// ND1212:AvoidEmptyInterfaces -warnif count > 0 from t in JustMyCode.Types where - t.IsInterface && - t.NbMethods == 0 && - !t.InterfacesImplemented.Any() && - t.Name != "Lerp" && - t.Name != "Component" && - (t.BaseClass != null && t.BaseClass.Name != "Component") && - !t.FullName.Contains("Component") && - !t.FullName.Contains("Message") && - !t.FullName.Contains("Trigger") && - !t.FullName.Contains(".Tests") && - t.Name != "SceneEntityTemplateModifiers" && - t.Name != "SceneEntityInstancesCreator" && - t.Name != "ContentMessage" -select new { - t, - t.TypesThatImplementMe, - Debt = (10 + 3*t.TypesThatImplementMe.Count()).ToMinutes().ToDebt(), - Severity = t.TypesThatImplementMe.Any() ? Severity.Medium : Severity.Low -} - -// -// Interfaces define members that provide a behavior or usage contract. -// The functionality that is described by the interface -// can be adopted by any type, regardless of where the type -// appears in the inheritance hierarchy. -// A type implements an interface by providing implementations -// for the members of the interface. -// An empty interface does not define any members. -// Therefore, it does not define a contract that can be implemented. -// -// If your design includes empty interfaces that types -// are expected to implement, you are probably using an interface -// as a marker or a way to identify a group of types. -// If this identification will occur at run time, -// the correct way to accomplish this is to use a custom attribute. -// Use the presence or absence of the attribute, -// or the properties of the attribute, to identify the target types. -// If the identification must occur at compile time, -// then it is acceptable to use an empty interface. -// -// Note that if an interface is empty but implements at least one -// other interface, it won't be matched by this rule. -// Such interface can be considered as not empty, -// since implementing it means that sub-interfaces members -// must be implemented. -// - -// -// Remove the interface or add members to it. -// If the empty interface is being used to label a set of types, -// replace the interface with a custom attribute. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 10 minutes to discard an empty interface plus -// 3 minutes per type implementing an empty interface. -//]]> - Avoid types initialization cycles -// ND1213:AvoidTypesInitializationCycles -warnif count > 0 - -// Types initialization cycle can only happen between types of an assembly. -from assembly in Application.Assemblies - -let cctorSuspects = assembly.ChildMethods.Where( - m => m.IsClassConstructor && - // Optimization: types involved in a type cycle necessarily don't have type level. - m.ParentType.Level == null) - -where cctorSuspects.Count() > 1 -let typesSuspects = cctorSuspects.ParentTypes().ToHashSetEx() - -// -// dicoTmp associates to each type suspect T, a set of types from typesSuspects -// that contains at least a method or a field used directly or indirectly by the cctor of T. -// -let dicoTmp = cctorSuspects.ToDictionary( - cctor => cctor.ParentType, - cctor => ((IMember)cctor).ToEnumerable().FillIterative( - members => from m in members - from mUsed in (m is IMethod) ? (m as IMethod).MembersUsed : new IMember[0] - where mUsed.ParentAssembly == assembly - select mUsed) - .DefinitionDomain - .Select(m => m.ParentType) // Don't need .Distinct() here, because of ToHashSetEx() below. - .Except(cctor.ParentType) - .Intersect(typesSuspects) - .ToHashSetEx() -) - -// -// dico associates to each type suspect T, the set of types initialized (directly or indirectly) -// by the initialization of T. This second step is needed, because if a cctor of a type T1 -// calls a member of a type T2, not only the cctor of T1 triggers the initialization of T2, -// but also it triggers the initialization of all types that are initialized by T2 initialization. -// -let dico = typesSuspects.Where(t => dicoTmp[t].Count() > 0).ToDictionary( - typeSuspect => typeSuspect, - typeSuspect => typeSuspect.ToEnumerable().FillIterative( - types => from t in types - from tUsed in dicoTmp[t] - select tUsed) - .DefinitionDomain - .Except(typeSuspect) - .ToHashSetEx() -) - - -// -// Now that dico is prepared, detect the cctor cycles -// -from t in dico.Keys - - // Thanks to the work done to build dico, it is now pretty easy - // to spot types involved in an initialization cyle with t! - let usersAndUseds = from tTmp in dico[t] - where dico.ContainsKey(tTmp) && dico[tTmp].Contains(t) - select tTmp - where usersAndUseds.Count() > 0 - - // Here we've found type(s) both using and used by the suspect type. - // A cycle involving the type t is found! - // v2017.3.2: don't call Append() as an extension method else ambiguous syntax error - // with the new extension method in .NET Fx v4.7.1 / .NET Standard 2.0: System.Linq.Enumerable.Append() - let typeInitCycle = ExtensionMethodsEnumerable.Append(usersAndUseds,t) - - - // Compute methodsCalled and fieldsUsed, useful to explore - // how a cctor involved in a type initialization cycle, triggers other type initialization. - let methodsCalledDepth = assembly.ChildMethods.DepthOfIsUsedBy(t.ClassConstructor) - let fieldsUsedDepth = assembly.ChildFields.DepthOfIsUsedBy(t.ClassConstructor) - - let methodsCalled = methodsCalledDepth.DefinitionDomain.OrderBy(m => methodsCalledDepth[m]).ToArray() - let fieldsUsed = fieldsUsedDepth.DefinitionDomain.OrderBy(f => fieldsUsedDepth[f]).ToArray() - -// Use the tick box to: Group cctors methods By parent types -select new { - t.ClassConstructor, - cctorsCycle= typeInitCycle.Select(tTmp => tTmp.ClassConstructor), - - // methodsCalled and fieldsUsed are members used directly and indirectly by the cctor. - // Export these members to the dependency graph (right click the cell Export/Append … to the Graph) - // and see how the cctor trigger the initialization of other types - methodsCalled, - fieldsUsed, - Debt = (20+10*typeInitCycle.Count()).ToMinutes().ToDebt(), - Severity = Severity.Critical -} - -// -// The *class constructor* (also called *static constructor*, and named *cctor* in IL code) -// of a type, if any, is executed by the CLR at runtime, the first time the type is used. -// A *cctor* doesn't need to be explicitly declared in C# or VB.NET, to exist in compiled IL code. -// Having a static field inline initialization is enough to have -// the *cctor* implicitly declared in the parent class or structure. -// -// If the *cctor* of a type *t1* is using the type *t2* and if the *cctor* of *t2* is using *t1*, -// some type initialization unexpected and hard-to-diagnose buggy behavior can occur. -// Such a cyclic chain of initialization is not necessarily limited to two types -// and can embrace *N* types in the general case. -// More information on types initialization cycles can be found here: -// http://codeblog.jonskeet.uk/2012/04/07/type-initializer-circular-dependencies/ -// -// The present code rule enumerates types initialization cycles. -// Some *false positives* can appear if some lambda expressions are defined -// in *cctors* or in methods called by *cctors*. In such situation, this rule -// considers these lambda expressions as executed at type initialization time, -// while it is not necessarily the case. -// - -// -// Types initialization cycles create confusion and unexpected behaviors. -// If several states hold by several classes must be initialized during the first -// access of any of those classes, a better design option is to create a dedicated -// class whose responsibility is to initialize and hold all these states. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 20 minutes per cycle plus 10 minutes per type class constructor -// involved in the cycle. -//]]> - - - Avoid custom delegates -// ND1300:AvoidCustomDelegates - -warnif count > 0 -from t in JustMyCode.Types where t.IsDelegate - -let invokeMethod = (from m in t.Methods where m.SimpleName == "Invoke" select m).Single() -let signature1 = invokeMethod.Name.Substring( - invokeMethod.SimpleName.Length, - invokeMethod.Name.Length - invokeMethod.SimpleName.Length) - -// 'ref' and 'out' parameters cannot be supported -where !signature1.Contains("&") && t.ParentType.Name != "NativeMethods" && t.ParentType.Name != "WindowsHook" - -let signature2 = signature1.Replace("(","<").Replace(")",">") -let signature3 = signature2 == "<>" ? "" : signature2 -let resultTypeName = invokeMethod.ReturnType == null ? "????" : - invokeMethod.ReturnType.FullName == "System.Void" ? "" : - invokeMethod.ReturnType.Name -let replaceWith = - resultTypeName == "Boolean" && invokeMethod.NbParameters == 1 ? - "Predicate" + signature3 : resultTypeName == "" ? - "Action" + signature3 : invokeMethod.NbParameters ==0 ? - "Func<" + resultTypeName + ">" : - "Func" + signature3.Replace(">", "," + resultTypeName + ">") - -let methodsUser = t.TypesUsingMe.ChildMethods().Where(m => m.IsUsing(t)) - -select new { - t, - replaceWith, - methodsUser, - Debt = (5 + 3*methodsUser.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// Generic delegates sould be preferred over custom delegates. -// Generic delegates are: -// -// • *Action<…>* to represent any method with *void* return type. -// -// • *Func<…>* to represent any method with a return type. The last -// generic argument is the return type of the prototyped methods. -// -// • *Predicate* to represent any method that takes an instance -// of *T* and that returns a *boolean*. -// -// • Expression<…> that represents function definitions that can be -// compiled and subsequently invoked at runtime but can also be -// serialized and passed to remote processes. -// -// Thanks to generic delegates, not only the code using these custom -// delegates will become clearer, but you'll be relieved from the -// maintenance of these delegate types. -// -// Notice that delegates that are consumed by *DllImport* extern methods -// must not be converted, else this could provoke marshalling issues. -// - -// -// Remove custom delegates and replace them with generic -// delegates shown in the **replaceWith** column. -// -// The estimated Debt, which means the effort to fix such issue, -// is 5 minutes per custom delegates plus 3 minutes per method -// using such custom delegate. -//]]> - Types with disposable instance fields must be disposable -// ND1301:TypesWithDisposableInstanceFieldsMustBeDisposable -warnif count > 0 - -// Several IDisposable types can be found if several .NET profiles are referenced. -let iDisposables = ThirdParty.Types.WithFullName("System.IDisposable") -where iDisposables.Any() // in case the code base doesn't use at all System.IDisposable - -from t in Application.Types.Except( - Application.Types.ThatImplementAny(iDisposables) - // Don't match ASP.NET types like Page, MasterPage or Control - .Union(Application.Types.Where(t => t.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWith("System.Web.UI"))))) -where !t.IsGeneratedByCompiler - -let instanceFieldsDisposable = - t.InstanceFields.Where(f => f.FieldType != null && - f.FieldType.InterfacesImplemented.Intersect(iDisposables).Any()) - -where instanceFieldsDisposable.Any() && - t.Name != "Component" && - (t.BaseClass != null && t.BaseClass.Name != "Component") && - !t.FullName.Contains("Component") && - !t.FullName.Contains("Message") && - !t.FullName.Contains("Trigger") && - !t.FullName.Contains("Entity") && - !t.FullName.Contains(".Tests") && - !t.IsGeneratedByCompiler && - t.Name != "ThreadStatic" && - t.Name != "TestWithMocksOrVisually" && - t.Name != "TestWithMocksOrVisually" && - t.Name != "TestTachyonServer" && - t.Name != "SceneEntities" && - t.Name != "ShaderParameters" && - !t.Name.Contains("Resolver") - -// Don't warn for types that implement the dispose pattern with a Dispose(bool) method. -where t.Methods.FirstOrDefault(m => m.Name == "Dispose(Boolean)") == null - -select new { - t, - instanceFieldsDisposable, - Debt = (5 + 2*instanceFieldsDisposable.Count()).ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns when a class declares and implements an instance field that -// is a *System.IDisposable* type and the class does not implement *IDisposable*. -// -// A class implements the *IDisposable* interface to dispose of unmanaged resources -// that it owns. An instance field that is an *IDisposable* type indicates that -// the field owns an unmanaged resource. A class that declares an *IDisposable* -// field indirectly owns an unmanaged resource and should implement the -// *IDisposable* interface. If the class does not directly own any unmanaged -// resources, it should not implement a finalizer. -// -// This rules might report false positive in case the lifetime of the disposable -// objects referenced, is longer than the lifetime of the object that hold the -// disposable references. -// - -// -// To fix a violation of this rule, implement *IDisposable* and from the -// *IDisposable.Dispose()* method call the *Dispose()* method of the field(s). -// -// The estimated Debt, which means the effort to fix such issue, -// is 5 minutes per type matched plus 3 minutes per disposable instance field. -//]]> - Disposable types with unmanaged resources should declare finalizer -// ND1302:DisposableTypesWithUnmanagedResourcesShouldDeclareFinalizer - -// This default rule is disabled by default, -// see in the rule description (below) why. -// warnif count > 0 - -// Several IDisposable type can be found if several .NET Fx are referenced. -let iDisposables = ThirdParty.Types.WithFullName("System.IDisposable") -where iDisposables.Any() // in case the code base doesn't use at all System.IDisposable - -let disposableTypes = Application.Types.ThatImplementAny(iDisposables) -let unmanagedResourcesFields = disposableTypes.ChildFields().Where(f => - !f.IsStatic && - f.FieldType != null && - f.FieldType.FullName.EqualsAny( - "System.IntPtr", - "System.UIntPtr", - "System.Runtime.InteropServices.HandleRef")).ToHashSetEx() -let disposableTypesWithUnmanagedResource = unmanagedResourcesFields.ParentTypes() - -from t in disposableTypesWithUnmanagedResource -where !t.HasFinalizer -let unmanagedResourcesTypeFields = unmanagedResourcesFields.Intersect(t.InstanceFields) -select new { - t, - unmanagedResourcesTypeFields, - //Debt = 10.ToMinutes().ToDebt(), - //Severity = Severity.Critical -} - -// -//A type that implements *System.IDisposable*, -//and has fields that suggest the use of unmanaged resources, -//does not implement a finalizer as described by *Object.Finalize()*. -//A violation of this rule is reported -//if the disposable type contains fields of the following types: -// -// • *System.IntPtr* -// -// • *System.UIntPtr* -// -// • *System.Runtime.InteropServices.HandleRef* -// -// Notice that this default rule is disabled by default, -// because it typically reports *false positive* for classes -// that just hold some references to managed resources, -// without the responsibility to dispose them. -// -// To enable this rule just uncomment *warnif count > 0*. -// - -// -//To fix a violation of this rule, -//implement a finalizer that calls your *Dispose()* method. -//]]> - Methods that create disposable object(s) and that don't call Dispose() -// ND1303:MethodsThatCreateDisposableObjectAndThatDontCallDispose - -// Uncomment this to transform this code query into a code rule. -// warnif count > 0 - -// Several IDisposable types can be found if several .NET Fx are referenced. -let iDisposables = ThirdParty.Types.WithFullName("System.IDisposable") -where iDisposables.Any() // in case the code base doesn't use at all System.IDisposable - -// Build sequences of disposableTypes and disposeMethods -let disposableTypes = Types.ThatImplementAny(iDisposables).Concat(iDisposables) -let disposeMethods = disposableTypes.ChildMethods().WithName("Dispose()").ToHashSetEx() - - -// -> You can refine this code query by assigning to disposableTypesToLookAfter something like: -// disposableTypes.WithFullNameIn("Namespace.TypeName1", "Namespace.TypeName2", ...) -let disposableTypesToLookAfter = disposableTypes - - -// -> You can refine this code query by assigning to methodsToLookAfter something like: -// Application.Assemblies.WithNameLike("Asm").ChildMethods() -let methodsToLookAfter = Application.Methods - - -// Enumerate methods that create any disposable type, without calling Dispose() -from m in methodsToLookAfter.ThatCreateAny(disposableTypesToLookAfter ) - -where !m.MethodsCalled.Intersect(disposeMethods).Any() -select new { - m, - disposableObjectsCreated = disposableTypes.Where(t => m.CreateA(t)), - m.MethodsCalled, - //Debt = 10.ToMinutes().ToDebt(), - //Severity = Severity.Low -} - -// -// This code query enumerates methods that create one or several disposable object(s), -// without calling any Dispose() method. -// -// This code query is not a code rule because it is acceptable to do so, -// as long as disposable objects are disposed somewhere else. -// -// This code query is designed to be be easily refactored -// to look after only specific disposable types, or specific caller methods. -// -// You can then refactor this code query to adapt it to your needs and transform it into a code rule. -//]]> - Classes that are candidate to be turned into structures -// ND1304:ClassesThatAreCandidateToBeTurnedIntoStructures - -warnif count > 0 -from t in JustMyCode.Types where - t.IsClass && - !t.IsGeneratedByCompiler && - !t.IsStatic && - !t.IsGeneric && - - t.NbChildren == 0 && // Must not have children - - t.Constructors.All(c => c.NbParameters > 0) && // Must not have parameterless ctor, struct cannot have custom parameterless ctor - - t.IsImmutable && // Structures should be immutable type. - - // Must have fields and all fields must be of value-type - t.InstanceFields.Count() > 0 && - t.InstanceFields.All(f => f.FieldType != null) && - t.InstanceFields.All(f => f.FieldType.IsStructure || f.FieldType.IsEnumeration) && - - // Must not implement interfaces to avoid boxing mismatch - // when structures implements interfaces. - t.InterfacesImplemented.Count() == 0 && - - // Must derive directly from System.Object - t.DepthOfDeriveFrom("System.Object".AllowNoMatch()) == 1 && - - // Must not be a serializable class because a structure should be immutable - // and serialized types are mutable. - !t.TypesUsed.Any(t1 => t1.IsAttributeClass && t1.ParentNamespace.Name == "Newtonsoft.Json") && - !t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) && - !t.IsUsing("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) && - !t.IsUsing("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) && - !t.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) && - !t.IsUsing("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) && - - // ASP.NET Core ViewModel and Repository - !t.SimpleName.EndsWithAny("Model","Repository") && - !t.IsUsing("System.ComponentModel.DataAnnotations".AllowNoMatch().MatchNamespace()) && - // Must not be an entry point class like 'Program' - !t.Methods.Any(m => m.IsEntryPoint) && - t.SizeOfInst < 16 - - // && t.IsSealed <-- You might want to add this condition - // to restraint the set. - // && !t.IsPubliclyVisible <-- You might want to add this condition if - // you are developping a framework with classes - // that are intended to be sub-classed by - // your clients. - let methodsUser = t.TypesUsingMe.ChildMethods().Where(m => m.IsUsing(t)) - -select new { - t, - t.SizeOfInst, - t.InstanceFields, - methodsUser, - Debt = (5 + 1*methodsUser.Count()).ToMinutes().ToDebt(), - Severity = Severity.Info -} - -// -// *Int32*, *Double*, *Char* or *Boolean* are structures and not classes. -// Structures are particularly suited to implement **lightweight values**. -// Hence a class is candidate to be turned into a structure -// when its instances are *lightweight values*. -// -// This is a matter of *performance*. It is expected that a program -// works with plenty of *short lived lightweight values*. -// In such situation, the advantage of using *struct* instead of -// *class*, (in other words, the advantage of using *values* instead -// of *objects*), is that *values* are not managed by the garbage collector. -// This means that values are cheaper to deal with. -// -// This rule matches classes that looks like being *lightweight values*. -// The characterization of such class is: -// -// • It has instance fields. -// -// • All instance fields are typed with value-types (primitive, structure or enumeration) -// -// • It is immutable (the value of its instance fields cannot be modified once the constructor ended). -// -// • It implements no interfaces. -// -// • It has no parameterless construtor. -// -// • It is not generic. -// -// • It has no derived classes. -// -// • It derives directly from *System.Object*. -// -// • ASP.NETCore ViewModel (its name ends with *Model*) and Repository -// -// This rule doesn't take account if instances of matched -// classes are numerous *short-lived* objects. -// These criterions are just indications. Only you can decide if it is -// *performance wise* to transform a class into a structure. -// -// A related case-study of using *class* or *struct* for *Tuple<…>* generic -// types can be found here: -// http://stackoverflow.com/questions/2410710/why-is-the-new-tuple-type-in-net-4-0-a-reference-type-class-and-not-a-value-t -// - -// -// Just use the keyword *struct* instead of the keyword *class*. -// -// **CAUTION:** Before applying this rule, make sure to understand -// the **deep implications** of transforming a class into a structure. -// http://msdn.microsoft.com/en-us/library/aa664471(v=vs.71).aspx -// -// The estimated Debt, which means the effort to fix such issue, -// is 5 minutes per class matched plus one minute per method -// using such class transformed into a structure. -//]]> - Avoid namespaces with few types -// ND1305:AvoidNamespacesWithFewTypes - -warnif count > 0 - -// Common infrastructure namespace names not matched by the rule. -// Complete this list to your need. -let infraNamespaceNames = new HashSet() { - "Services","Exceptions","Logging", "Domain", - "Identity", "Migrations", "Controllers", - "Specifications", "Interfaces", "Components", - "Bus", "Models", "EventHandlers", "Mappings", - "ViewModels", "ViewComponents", "Notifications", - "Configurations", "Extensions", "Events", - "Context", "Data", "EventSourcing", "Repository", - "IoC", "Manage", "Commands", "CommandHandlers", - "Validations", "EventStoreSQL", "Authorization", - "Formatters", "Xml", "Json", "Enums", - "Abstractions", "BaseClasses", "Settings", - "Helpers", "Validators", "Entities", "Ads", "Web", - "Networking", "Analytics", "Log", "Templates", - "Authentication", "Achievements", "DepthFilters" -} - -from n in JustMyCode.Namespaces -where n.Name.Length > 0 // Don't match anonymous namespaces - && !infraNamespaceNames.Contains(n.SimpleName) // Don't match common infrastructure namespaces - && n.Name != n.ParentAssembly.Name && // Don't warn on namespace named as assembly to avoid warning on new VS projects - n.Name != "" && - !n.Name.Contains(".Tests") && - !n.Name.Contains(".Mocks") && - !n.Name.Contains(".Templates") && - !n.Name.Contains(".Components") && - !n.Name.Contains(".Tutorials.") && - !n.Name.Contains(".Content") && - !n.Name.Contains(".MusicStreams") && - !n.Name.Contains(".Optional") && - !n.Name.Contains(".Log") && - !n.Name.Contains(".Custom") && - !n.Name.Contains(".Frameworks") -let types = n.ChildTypes.Where(t => !t.IsGeneratedByCompiler).ToArray() -where - types.Length > 0 && // Don't match namespaces that contain only types GeneratedByCompiler - types.Length < 5 && - (types.Count() == 0 || types.First().Name != "Program" && types.First().Name != "Scene") && - // Only match namespaces that have all types in JustMyCode - types.All(t => JustMyCode.Contains(t)) - orderby types.Length ascending -select new { - n, - types, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This rule warns about namespaces other than the global namespace -// that contain less than five types. -// -// Make sure that each of your namespaces has a logical organization -// and that a valid reason exists to put types in a sparsely -// populated namespace. -// -// Namespaces should contain types that are used together in most -// scenarios. When their applications are mutually exclusive, -// types should be located in separate namespaces. For example, -// the *System.Web.UI* namespace contains types that are used -// in Web applications, and the *System.Windows.Forms* namespace -// contains types that are used in Windows-based applications. -// Even though both namespaces have types that control aspects -// of the user interface, these types are not designed for -// use in the same application. Therefore, they are located in -// separate namespaces. -// -// Careful namespace organization can also be helpful because -// it increases the discoverability of a feature. By examining the -// namespace hierarchy, library consumers should be able to locate -// the types that implement a feature. -// -// Notice that this rule source code contains a list of common -// infrastructure namespace names that you can complete. -// Namespaces with ending name component in this list are not matched. -// - -// -// To fix a violation of this rule, try to combine namespaces -// that contain just a few types into a single namespace. -// -]]> - Nested types should not be visible -// ND1306:NestedTypesShouldNotBeVisible - -warnif count > 0 from t in JustMyCode.Types where - t.IsNested && - !t.IsGeneratedByCompiler && - !t.IsPrivate && - !t.FullName.Contains("CircularBuffer") && - !t.FullName.Contains("Scene") && - !t.FullName.Contains("Tests") && - !t.FullName.Contains("Mocks") && - !t.FullName.Contains("NativeMethods") && - !t.FullName.Contains("Context") - -let typesUser = t.TypesUsingMe.Where(t1 => t1 != t.ParentType && t1.ParentType != t.ParentType) -select new { - t, - t.Visibility, - typesUser, - Debt = (2 + 4*typesUser.Count()).ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about nested types not declared as private. -// -// A nested type is a type declared within the scope of another -// type. Nested types are useful for encapsulating private -// implementation details of the containing type. Used -// for this purpose, nested types should not be externally visible. -// -// Do not use externally visible nested types for logical -// grouping or to avoid name collisions; instead use namespaces. -// -// Nested types include the notion of member accessibility, -// which some programmers do not understand clearly. -// -// Protected types can be used in subclasses and nested types -// in advanced customization scenarios. -// - -// -// If you do not intend the nested type to be externally visible, -// change the type's accessibility. -// -// Otherwise, remove the nested type from its parent and make it -// *non-nested*. -// -// If the purpose of the nesting is to group some nested types, -// use a namespace to create the hierarchy instead. -// -// The estimated Debt, which means the effort to fix such issue, -// is 2 minutes per nested type plus 4 minutes per outter type -// using such nesting type. -//]]> - Declare types in namespaces -// ND1307:DeclareTypesInNamespaces - -warnif count > 0 from n in Application.Namespaces where - // If an anonymous namespace can be found, - // it means that it contains types outside of namespaces. - n.Name == "" - - // Eliminate anonymous namespaces that contains - // only notmycode types (like generated types). - let childTypes = n.ChildTypes.Where(t => JustMyCode.Contains(t)) - where childTypes.Count() > 0 -select new { - n, - childTypes, - n.NbLinesOfCode, - Debt = 2*childTypes.Count().ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Types are declared within namespaces to prevent name collisions, -// and as a way of organizing related types in an object hierarchy. -// -// Types outside any named namespace are in a *global -// namespace* that cannot be referenced in code. -// -// The *global namespace* has no name, hence it is qualified as -// being the *anonymous namespace*. -// -// This rule warns about *anonymous namespaces*. -// - -// -// To fix a violation of this rule, -// declare all types of all anonymous -// namespaces in some named namespaces. -//]]> - Empty static constructor can be discarded -// ND1308:EmptyStaticConstructorCanBeDiscarded - -warnif count > 0 from m in JustMyCode.Methods where - m.IsClassConstructor && - m.NbLinesOfCode == 0 -select new { - m, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// The *class constructor* (also called *static constructor*, and named *cctor* in IL code) -// of a type, if any, is executed by the CLR at runtime, just before the first time the type is used. -// -// This rule warns about the declarations of *static constructors* -// that don't contain any lines of code. Such *cctors* are useless -// and can be safely removed. -// - -// -// Remove matched empty *static constructors*. -//]]> - Instances size shouldn't be too big -// ND1309:InstancesSizeShouldntBeTooBig - -warnif count > 0 from t in JustMyCode.Types where - t.SizeOfInst > 128 && - !t.FullName.Contains("Renderer") && - !t.FullName.Contains("DataProperty") && - !t.FullName.EndsWith("DataProcessor") && - !t.FullName.Contains("Matrix") && - !t.FullName.Contains("Test") && - !t.FullName.Contains("Mock") && - !t.FullName.Contains("Graphics") && - !t.FullName.Contains("Frameworks") && - !t.FullName.Contains("Optional") && - !t.FullName.Contains(".Scene") && - !t.Name.Contains("Resolver") && - !t.Name.Contains("Runner") && - !t.Name.Contains("SceneContentEntityRepository") && - !t.Name.Contains("VorbisStreamDecoder") && - - // You might want to restrict this rule only on structure, since the cost of copying instance data at each method call might be prohibitive - // t.IsStructure && - - // Discard types that represent WPF, WindowsForm and ASP.NET forms and controls and EntityFramwork classes. - t.BaseClasses.All(bc => !bc.ParentNamespace.Name.StartsWithAny( - "System.Windows", "System.Web.UI", "System.Web", "Microsoft.EntityFramework", - // Discard types related to these namespaces that typically require large instances size - "System.ComponentModel", "System.Xml", - // Just add more component vendors here if needed - "DevExpress")) - - orderby t.SizeOfInst descending -select new { - t, - t.SizeOfInst, - t.InstanceFields, - t.BaseClasses, - Debt = t.SizeOfInst.Linear(128,10, 2048, 120).ToMinutes().ToDebt(), - - // The annual interest varies linearly from interests for severity Medium for 64 bytes per instance - // to twice interests for severity High for 2048 bytes per instance - AnnualInterest = (t.SizeOfInst.Linear(128, Severity.Medium.AnnualInterestThreshold().Value.TotalMinutes, - 2048, 2*(Severity.High.AnnualInterestThreshold().Value.TotalMinutes)) - )*(t.IsStructure ? 10 : 1) // Multiply interest by 10 for structures - .ToMinutes().ToAnnualInterest() - -} - -// -// Types where *SizeOfInst > 128* might degrade performance -// if many instances are created at runtime. -// They can also be hard to maintain. -// -// Notice that a class with a large *SizeOfInst* value -// doesn't necessarily have a lot of instance fields. -// It might derive from a class with a large *SizeOfInst* value. -// -// This query doesn't match types that represent WPF -// and WindowsForm forms and controls nor Entity Framework -// special classes. -// -// Some other namespaces like *System.ComponentModel* or *System.Xml* -// have base classes that typically imply large instances size -// so this rule doesn't match these situations. -// -// This rule doesn't match custom *DevExpress* component -// and it is easy to modify this rule ro append other component vendors -// to avoid false positives. -// -// See the definition of the *SizeOfInst* metric here -// https://www.ndepend.com/docs/code-metrics#SizeOfInst -// - -// -// A type with a large *SizeOfInst* value hold *directly* -// a lot of data. Typically, you can group this data into -// smaller types that can then be composed. -// -// The estimated Debt, which means the effort to fix such issue, -// varies linearly from severity **Medium** for 128 bytes per instance -// to twice interests for severity **High** for 2048 bytes per instance. -// -// The estimated annual interest of issues of this rule is 10 times higher -// for structures, because large structures have a significant performance cost. -// Indeed, each time such structure *value* is passed as a method parameter -// it gets copied to a new local variable -// (note that the word *value* is more appropriate than the word *instance* for structures). -// For this reason, such structure should be declared as class. -//]]> - Attribute classes should be sealed -// ND1310:AttributeClassesShouldBeSealed - -warnif count > 0 from t in Application.Types where - t.IsAttributeClass && - !t.IsSealed && - !t.IsAbstract && - t.IsPublic -select new { - t, - t.NbLinesOfCode, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// The .NET Framework class library provides methods -// for retrieving custom attributes. By default, -// these methods search the attribute inheritance -// hierarchy; for example -// *System.Attribute.GetCustomAttribute()* -// searches for the specified attribute type, or any -// attribute type that extends the specified attribute -// type. -// -// Sealing the attribute eliminates the search -// through the inheritance hierarchy, and can improve -// performance. -// - -// -// To fix a violation of this rule, seal the attribute -// type or make it abstract. -//]]> - Do implement methods that throw NotImplementedException -// ND1312:DoImplementMethodsThatThrowNotImplementedException - -warnif count > 0 -from m in Application.Methods -where m.CreateA("System.NotImplementedException".AllowNoMatch()) -select new { - m, - m.NbLinesOfCode, - Debt = (m.NbLinesOfCode == 1 ? 10 : 3).ToMinutes().ToDebt(), - Severity = m.NbLinesOfCode == 1 ? Severity.High : Severity.Medium -} - -// -// The exception *NotImplementedException* is used to declare -// a method *stub* that can be invoked, and defer the -// development of the method implementation. -// -// This exception is especially useful when doing **TDD** -// (*Test Driven Development*) when tests are written first. -// This way tests fail until the implementation is written. -// -// Hence using *NotImplementedException* is a *temporary* -// facility, and before releasing, will come a time when -// this exception shouldn't be used anywhere in code. -// -// *NotImplementedException* should not be used permanently -// to mean something like *this method should be overriden* -// or *this implementation doesn't support this facility*. -// Artefact like *abstract method* or *abstract class* should -// be used instead, to favor a *compile time* error over a -// *run-time* error. -// -// This rule warns about method still using -// *NotImplementedException*. -// - -// -// Investigate why *NotImplementedException* is still -// thrown. -// -// Such issue has a **High** severity if the method code -// consists only in throwing *NotImplementedException*. -// Such situation means either that the method should be -// implemented, either that what should be a *compile time* -// error is a *run-time* error *by-design*, -// and this is not good design. Sometime this situation -// also pinpoints a method stub that can be safely removed. -// -// If *NotImplementedException* is thrown from a method -// with significant logic, the severity is considered as -// **Medium**, because often the fix consists in throwing -// another exception type, like **InvalidOperationException**. -//]]> - Don't use obsolete types, methods or fields -// ND1311:DontUseObsoleteTypesMethodsOrFields - -warnif count > 0 -let obsoleteTypes = Types.Where(t => t.IsObsolete) -let obsoleteMethods = Methods.Where(m => m.IsObsolete).ToHashSetEx() -let obsoleteFields = Fields.Where(f => f.IsObsolete) - -from m in JustMyCode.Methods.UsingAny(obsoleteTypes).Union( - JustMyCode.Methods.UsingAny(obsoleteMethods)).Union( - JustMyCode.Methods.UsingAny(obsoleteFields)) -let obsoleteTypesUsed = obsoleteTypes.UsedBy(m) - -// Optimization: MethodsCalled + Intersect() is faster than using obsoleteMethods.UsedBy() -let obsoleteMethodsUsed = m.MethodsCalled.Intersect(obsoleteMethods) -let obsoleteFieldsUsed = obsoleteFields.UsedBy(m) - -let obsoleteUsage = obsoleteTypesUsed.Cast().Concat(obsoleteMethodsUsed).Concat(obsoleteFieldsUsed) - -select new { - m, - obsoleteUsage, - Debt = (5*obsoleteUsage.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// The attribute *System.ObsoleteAttribute* is used to tag -// types, methods or fields of an API that clients shouldn't -// use because these code elements will be removed sooner -// or later. -// -// This rule warns about methods that use a type, a method -// or a field, tagged with *System.ObsoleteAttribute*. -// - -// -// Typically when a code element is tagged with -// *System.ObsoleteAttribute*, a *workaround message* -// is provided to clients. -// -// This *workaround message* will tell you what to do -// to avoid using the obsolete code element. -// -// The estimated Debt, which means the effort to fix such issue, -// is 5 minutes per type, method or field used. -// -// Issues of this rule have a severity **High** -// because it is important to not rely anymore on obsolete code. -//]]> - Override equals and operator equals on value types -// ND1313:OverrideEqualsAndOperatorEqualsOnValueTypes - -warnif count > 0 -from t in JustMyCode.Types where - t.IsStructure && - !t.Name.EndsWith("NativeMethods") && - !t.FullName.Contains("Tests") && - !t.ParentNamespace.Name.EndsWith("Vertices") && - !t.FullName.Contains("NVorbis") && - !t.FullName.Contains("Frameworks") && - !t.FullName.Contains("WebCam") && - t.Name != "Circle" && - t.Name != "Pixel" && - t.Name != "MergedPixel" && - t.InstanceFields.Count() > 0 -let equalsMethod = t.InstanceMethods.Where(m0 => m0.Name == "Equals(Object)").SingleOrDefault() -where equalsMethod == null -select new { - t, - t.InstanceFields, - Debt = (15 + 2*t.InstanceFields.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// For value types, the inherited implementation of *Equals()* uses -// the Reflection library, and compares the contents of all instances -// fields. Reflection is computationally expensive, and comparing -// every field for equality might be unnecessary. -// -// If you expect users to compare or sort instances, or use them -// as hash table keys, your value type should implement *Equals()*. -// In C# and VB.NET, you should also provide an implementation of -// *GetHashCode()* and of the equality and inequality operators. -// - -// -// To fix a violation of this rule, provide an implementation -// of *Equals()* and *GetHashCode()* and implement the equality -// and inequality operators. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 15 minutes plus 2 minutes per instance field. -//]]> - Boxing/unboxing should be avoided -// ND1314:BoxingUnboxingShouldBeAvoided -warnif count > 0 from m in JustMyCode.Methods where - (m.IsUsingBoxing || - m.IsUsingUnboxing) && - !m.ParentType.FullName.Contains("Extensions") && - !m.ParentType.FullName.Contains("Datatypes") && - !m.ParentType.Name.StartsWith("<") -select new { - m, - m.NbLinesOfCode, - m.IsUsingBoxing, - m.IsUsingUnboxing, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// *Boxing* is the process of converting a value type to the type -// *object* or to any interface type implemented by this value -// type. When the CLR boxes a value type, it wraps the value -// inside a *System.Object* and stores it on the managed heap. -// -// *Unboxing* extracts the value type from the object. Boxing -// is implicit; unboxing is explicit. -// -// The concept of boxing and unboxing underlies the C# unified -// view of the type system in which a value of any type can -// be treated as an object. More about *boxing* and *unboxing* -// here: https://msdn.microsoft.com/en-us/library/yz2be5wk.aspx -// -// **This rule has been disabled by default** to avoid noise in -// issues found by the NDepend default rule set. If boxing/unboxing -// is important to your team, just re-activate this rule. -// - -// -// Thanks to .NET generic, and especially thanks to -// generic collections, *boxing* and *unboxing* should -// be rarely used. Hence in most situations the code can -// be refactored to avoid relying on *boxing* and *unboxing*. -// See for example: -// http://stackoverflow.com/questions/4403055/boxing-unboxing-and-generics -// -// With a performance profiler, indentify methods that consume -// a lot of CPU time. If such method uses *boxing* or -// *unboxing*, especially in a **loop**, make sure to refactor it. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - - - Avoid namespaces mutually dependent -// ND1400:AvoidNamespacesMutuallyDependent - -warnif count > 0 - -// Optimization: restraint application assemblies set -// If some namespaces are mutually dependent -// - They must be declared in the same assembly -// - The parent assembly must ContainsNamespaceDependencyCycle -from assembly in Application.Assemblies.Where(a => a.ContainsNamespaceDependencyCycle != null && a.ContainsNamespaceDependencyCycle.Value) - -// hashset is used to avoid reporting both A <-> B and B <-> A -let hashset = new HashSet() - -// Optimization: restraint namespaces set -let namespacesSuspect = assembly.ChildNamespaces.Where( - // If a namespace doesn't have a Level value, it must be in a dependency cycle - // or it must be using directly or indirectly a dependency cycle. - n => n.Level == null && - // Also require the namespace to be JustMyCode to avoid generated namespaces like My / My.Resources in VB.NET - JustMyCode.Contains(n)) - -from nA in namespacesSuspect -where - !nA.Name.EndsWith("Templates") && - !nA.Name.Contains("Components") && - !nA.Name.Contains("Mocks") - -// Select namespaces mutually dependent with nA -let unused = hashset.Add(nA) // Populate hashset -let namespacesMutuallyDependentWith_nA = nA.NamespacesUsed.Using(nA) - .Except(hashset) // <-- avoid reporting both A <-> B and B <-> A -where namespacesMutuallyDependentWith_nA.Count() > 0 - -from nB in namespacesMutuallyDependentWith_nA -where - !nB.Name.EndsWith("Controls") && - !nB.Name.EndsWith("Templates") && - !nB.Name.Contains("Components") && - !nB.Name.Contains("Views") && - !nB.Name.Contains("Resolvers") - -// nA and nB are mutually dependent -// Infer which one is low level and which one is high level, -// for that we need to compute the coupling from A to B -// and from B to A in terms of number of types and methods -// usages involved in the coupling. -let typesOfBUsedByA = nB.ChildTypes.UsedBy(nA).Where(t=>!t.Name.StartsWith("Scene")).ToArray() // Enumerate once -let couplingA2B = - typesOfBUsedByA.Sum(t => t.TypesUsingMe.Count(t1 => t1.ParentNamespace == nA)) + - typesOfBUsedByA.ChildMethods().Sum(m => m.MethodsCallingMe.Count(m1 => m1.ParentNamespace == nA)) - -let typesOfAUsedByB = nA.ChildTypes.UsedBy(nB).Where(t=>!t.Name.StartsWith("Scene")).ToArray() // Enumerate once -let couplingB2A = - typesOfAUsedByB.Sum(t => t.TypesUsingMe.Count(t1 => t1.ParentNamespace == nB)) + - typesOfAUsedByB.ChildMethods().Sum(m => m.MethodsCallingMe.Count(m1 => m1.ParentNamespace == nB)) - -// The lowLevelNamespace is inferred from the fact that -// [coupling lowLevel -> highLevel] is lower than [coupling highLevel -> lowLevel] -let lowLevelNamespace = (couplingA2B < couplingB2A) ? nA : nB -let highLevelNamespace = (lowLevelNamespace == nA) ? nB : nA - -let highLevelTypesUsed = (lowLevelNamespace == nA) ? typesOfBUsedByA : typesOfAUsedByB -let lowLevelTypesUser = lowLevelNamespace.ChildTypes.UsingAny(highLevelTypesUsed) - -let lowLevelTypesMethodsUser = lowLevelTypesUser.Cast() - .Concat(lowLevelTypesUser.ChildMethods().Using(highLevelNamespace)) - .ToArray() // Enumerate once - -// Make the rule works also when lines of code is not available -let lowLevelNamespaceLoc = lowLevelNamespace.NbLinesOfCode ?? (lowLevelNamespace.NbILInstructions / 7) -let highLevelNamespaceLoc = highLevelNamespace.NbLinesOfCode ?? (highLevelNamespace.NbILInstructions / 7) - -let annualInterestPerIssue = - // Such issue has at least a Severity.Medium, never a Severity.Low - Math.Max(Severity.Medium.AnnualInterestThreshold().Value.TotalSeconds, - ((3600 + lowLevelNamespaceLoc + highLevelNamespaceLoc) / Math.Max(1,lowLevelTypesMethodsUser.Length)).Value) - .ToSeconds().ToAnnualInterest() - -// Select in details types and methods involved in the coupling lowLevelNamespace using highLevelNamespace -from tmCulprit in lowLevelTypesMethodsUser -let used = (tmCulprit.IsType ? tmCulprit.AsType.TypesUsed.Where(t => t.ParentNamespace == highLevelNamespace) : - tmCulprit.AsMethod.MembersUsed.Where(m => m.ParentNamespace == highLevelNamespace)) - .ToArray() // Enumerate once - -select new { - tmCulprit, - shouldntUse = used, - becauseNamespace = lowLevelNamespace, - shouldntUseNamespace = highLevelNamespace, - Debt = used.Length.Linear(1, 15, 10, 60).ToMinutes().ToDebt(), - AnnualInterest = annualInterestPerIssue - -} -// -// This rule lists types and methods from a low-level namespace -// that use types and methods from higher-level namespace. -// -// The pair of low and high level namespaces is made of two -// namespaces that use each other. -// -// For each pair of namespaces, to infer which one is low-level -// and which one is high-level, the rule computes the two coupling -// [from A to B] and [from B to A] in terms of number of types, -// methods and fields involved in the coupling. Typically -// the coupling from low-level to high-level namespace is significantly -// lower than the other legitimate coupling. -// -// Following this rule is useful to avoid **namespaces dependency -// cycles**. This will get the code architecture close to a -// *layered architecture*, where *low-level* code is not allowed -// to use *high-level* code. -// -// In other words, abiding by this rule will help significantly -// getting rid of what is often called **spaghetti code: -// Entangled code that is not properly layered and structured**. -// -// More on this in our white books relative to partitioning code. -// https://www.ndepend.com/docs/white-books -// - -// -// Refactor the code to make sure that **the low-level namespace -// doesn't use the high-level namespace**. -// -// The rule lists in detail which low-level types and methods -// shouldn't use which high-level types and methods. The refactoring -// patterns that help getting rid of each listed dependency include: -// -// • Moving one or several types from the *low-level* namespaces -// to the *high-level* one, or do the opposite. -// -// • Use *Inversion of Control (IoC)*: -// http://en.wikipedia.org/wiki/Inversion_of_control -// This consists in creating new interfaces in the -// *low-level* namespace, implemented by classes -// in the *high-level* namespace. This way *low-level* -// code can consume *high-level* code through interfaces, -// without using directly *high-level* implementations. -// Interfaces can be passed to *low-level* code through -// the *high-level* namespace code, or through even -// higher-level code. In related documentations -// you can see these interfaces named as *callbacks*, -// and the overall pattern is also known as -// *Dependency Injection (DI)*: -// http://en.wikipedia.org/wiki/Dependency_injection -// -// That rule might not be applicable for frameworks -// that present public namespaces mutually dependent. -// In such situation the cost to break the API can be -// higher than the cost to let the code entangled. -// -// - -// -// The estimated **Debt**, which means the effort to fix such issue -// to make sure that the first namespace doesn't rely anymore -// on the second one, depends on the number of types and methods used. -// -// Because both namespace are now forming a *super-component* -// that cannot be partitioned in smaller components, the cost to -// unfix each issue is proportional to the size of this super-component. -// As a consequence, the estimated **Annual Interest**, which means -// the annual cost to let both namespaces mutually dependend, is equal -// to an hour plus a number of minutes proportional to the size -// (in lines of code) of both namespaces. The obtained *Annual Interest* -// value is then divided by the number of detailled issues listed. -// -// Often the estimated *Annual Interest* for each listed issue -// is higher than the *Debt*, which means that leaving such issue -// unfixed for a year costs more than taking the time to fix issue once. -// -// -- -// -// To explore the coupling between the two namespaces mutually -// dependent: -// -// 1) from the *becauseNamespace right-click menu* choose -// *Copy to Matrix Columns* to export this low-level namespace -// to the horizontal header of the dependency matrix. -// -// 2) from the *shouldntUseNamespace right-click menu* choose -// *Copy to Matrix Rows* to export this high-level namespace to -// the vertical header of the dependency matrix. -// -// 3) double-click the black matrix cell (it is black because of -// the mutual dependency). -// -// 4) in the matrix command bar, click the button: -// *Remove empty Row(s) and Column(s)*. -// -// At this point, the dependency matrix shows types involved -// into the coupling. -// -// • Blue cells represent types from low-level namespace using types -// from high-level namespace -// -// • Green cells represent types from high-level namespace using -// types from low-level namespace -// -// • Black cells represent types from low-level and high-level -// namespaces that use each other. -// -// There are more green cells than blue and black cells because -// green cell represents correct coupling from high-level to low-level. -// **The goal is to eliminate incorrect dependencies represented by -// blue and black cells.** -// - ]]> - Avoid namespaces dependency cycles -// ND1401:AvoidNamespacesDependencyCycles - -warnif count > 0 - -// Optimization: restraint application assemblies set -// If some namespaces are mutually dependent -// - They must be declared in the same assembly -// - The parent assembly must ContainsNamespaceDependencyCycle -from assembly in Application.Assemblies - .Where(a => a.ContainsNamespaceDependencyCycle != null && - a.ContainsNamespaceDependencyCycle.Value) - -// Optimization: restraint namespaces set -let namespacesSuspect = assembly.ChildNamespaces.Where(n => - // A namespace involved in a cycle necessarily have a null Level. - n.Level == null && - // Also require the namespace to be JustMyCode to avoid generated namespaces like My / My.Resources in VB.NET - JustMyCode.Contains(n)) - -// hashset is used to avoid iterating again on namespaces already caught in a cycle. -let hashset = new HashSet() - - -from suspect in namespacesSuspect - // By commenting in this line, the query matches all namespaces involved in a cycle. - where !hashset.Contains(suspect) - - // Define 2 code metrics - // • Namespaces depth of is using indirectly the suspect namespace. - // • Namespaces depth of is used by the suspect namespace indirectly. - // Note: for direct usage the depth is equal to 1. - let namespacesUserDepth = namespacesSuspect.DepthOfIsUsing(suspect) - let namespacesUsedDepth = namespacesSuspect.DepthOfIsUsedBy(suspect) - - // Select namespaces that are both using and used by namespaceSuspect - let usersAndUsed = from n in namespacesSuspect where - namespacesUserDepth[n] > 0 && - namespacesUsedDepth[n] > 0 - select n - - where usersAndUsed.Count() > 0 - - // Here we've found namespace(s) both using and used by the suspect namespace. - // A cycle involving the suspect namespace is found! - // v2017.3.2: don't call Append() as an extension method else ambiguous syntax error - // with the new extension method in .NET Fx v4.7.1 / .NET Standard 2.0: System.Linq.Enumerable.Append() - let cycle = ExtensionMethodsEnumerable.Append(usersAndUsed,suspect) - - // Fill hashset with namespaces in the cycle. - // .ToArray() is needed to force the iterating process. - let unused1 = (from n in cycle let unused2 = hashset.Add(n) select n).ToArray() - -// This is hard to check, all cycle mentions have to be checked if any of them contain any other usings! -// Anyway, always ignore cycles that contain Templates or Components, or main projects -where !cycle.Any(n=> - !n.Name.Contains(".") || - n.Name.EndsWith("Templates") || - n.Name.Contains("Components") || - n.Name.Contains("Content") || - n.Name.Contains("Resolvers")) -select new { - suspect, - cycle, - Debt = 120.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule lists all *application namespace dependency cycles*. -// Each row shows a different cycle, indexed with one of the namespace entangled -// in the cycle. -// -// To browse a cycle on the dependency graph or the dependency matrix, right click -// a cycle cell and export the matched namespaces to the dependency graph or matrix. -// -// In the matrix, dependency cycles are represented with red squares and black cells. -// To easily browse dependency cycles, the dependency matrix comes with an option: -// *Display Direct and Indirect Dependencies* -// -// Read our white books relative to partitioning code, -// to know more about namespaces dependency cycles, and why avoiding them -// is a *simple yet efficient* solution to clean the architecture of a code base. -// https://www.ndepend.com/docs/white-books -// - -// -// Removing first pairs of *mutually dependent namespaces* will eliminate -// most *namespaces dependency cycles*. This is why it is recommended -// focusing on matches of the default rule -// **Avoid namespaces mutually dependent** before dealing -// with the present rule. -// -// Once solving all *mutually dependent namespaces*, remaining cycles -// matched by the present rule necessarily involve 3 or more namespaces -// like in: *A is using B is using C is using A*. -// Such cycle can be broken by identifying which namespace should -// be at the *lower-level*. For example if B should be at the -// *lower-level*, then it means C should be at the *higher-level* -// and to break the cycle, you just have to remove the dependency -// from B to C, with a pattern described in the *HowToFix* section -// of the rule *Avoid namespaces mutually dependent*. -// -// The estimated Debt, which means the effort to fix such issue, -// doesn't depend on the cycle length. First because fixing the rule -// **Avoid namespaces mutually dependent** will fix most cycle reported -// here, second because even a long cycle can be broken by removing -// a few dependency. -//]]> - Avoid partitioning the code base through many small library Assemblies -// ND1402:AvoidPartitioningTheCodeBaseThroughManySmallLibraryAssemblies - -warnif count > 10 -from a in Application.Assemblies where - !a.Name.Contains("Strict.") && - !a.Name.Contains(".Authentication") && - !a.Name.Contains(".Ads") && - !a.Name.Contains(".Analytics") && - !a.Name.Contains(".Achievements") && - !a.Name.Contains(".Authentication") && - !a.Name.Contains(".VideoInputs") && - !a.Name.Contains(".Mocks") && - !a.Name.Contains(".ImageProcessing") && - !a.Name.Contains(".Xml") && - !a.Name.Contains(".Multimedia") && - !a.Name.Contains(".Fonts") && - !a.Name.Contains(".WpfViewport") && - !a.Name.Contains("Modifiers") && - !a.Name.Contains("App") && - ( a.NbLinesOfCode < 500 || - a.NbILInstructions < 1000 ) && - a.FilePath.FileExtension.ToLower() == ".dll" -select new { - a, - a.NbLinesOfCode, - a.NbILInstructions, - Debt = 40.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Each .NET Assembly compiled represents one or several physical file(s). -// Having too many library .NET Assemblies is a symptom of -// considering **physical** .NET Assemblies as **logical** components. -// -// We advise having less, and bigger, .NET Assemblies -// and using the concept of namespaces to define logical components. -// Benefits are: -// -// • Faster compilation time. -// -// • Faster startup time for your program. -// -// • Easier deployment thanks to less files to manage. -// -// • If you are developing a Framework, -// less .NET assemblies to reference and manage for your clients. -// - -// -// Consider using the *physical* concept of assemblies for physical needs -// only. -// -// Our white book about **Partitioning code base through .NET assemblies -// and Visual Studio projects** explains in details valid and invalid -// reasons to use assemblies. -// Download it here: -// https://www.ndepend.com/Res/NDependWhiteBook_Assembly.pdf -//]]> - UI layer shouldn't use directly DB types -// ND1403:UILayerShouldntUseDirectlyDBTypes - -warnif count > 0 - -// UI layer is made of types using a UI framework -let uiTypes = Application.Types.UsingAny(Assemblies.WithNameIn("PresentationFramework", "System.Windows", "System.Windows.Forms", "System.Web")) - -// You can easily customize this part to define what are DB types. -let dbTypes = ThirdParty.Assemblies.WithNameIn("System.Data", "EntityFramework", "NHibernate").ChildTypes() - // Ideally even DataSet and associated, usage should be forbidden from UI layer: - // http://stackoverflow.com/questions/1708690/is-list-better-than-dataset-for-ui-layer-in-asp-net - .Except(ThirdParty.Types.WithNameIn("DataSet", "DataTable", "DataRow")) - -from uiType in uiTypes.UsingAny(dbTypes) -let dbTypesUsed = dbTypes.Intersect(uiType.TypesUsed) - -let dbTypesAndMembersUsed = dbTypesUsed.Union(dbTypesUsed.ChildMembers().UsedBy(uiType)) - -// Per defaut this rule estimates a technical debt -// proportional to the coupling between the UI and DB types. -let couplingPerUIType = 2 + - uiType.Methods.UsingAny(dbTypesUsed).Count() + - dbTypesAndMembersUsed.Count() - -select new { - uiType, - dbTypesAndMembersUsed, - Debt = (4 * couplingPerUIType).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is more a *sample rule to adapt to your need*, -// than a rigid rule you should abide by. It shows how to -// define code layers in a rule and how to be warned about -// layers dependencies violations. -// -// This rule first defines the UI layer and the DB framework -// layer. Second it checks if any UI layer type is using -// directly any DB framework layer type. -// -// • The **DB framework layer** is defined as the set of *third-party* -// types in the framework *ADO.NET*, *EntityFramework*, -// *NHibernate* types, that the application is consuming. -// It is easy to append and suppress any DB framework. -// -// • The UI layer (**User Interface Layer**) is defined as the -// set of types that use *WPF*, *Windows Form*, *ASP.NET*. -// -// *UI using directly DB frameworks* is generally considered -// as *poor design* because DB frameworks accesses should be -// a concept hidden to UI, encapsulated into a **dedicated -// Data Access Layer (DAL)**. -// -// Notice that per defaut this rule estimates a technical debt -// proportional to the coupling between the UI and DB types. -// - -// -// This rule lists precisely which UI type uses which -// DB framework type. Instead of fixing matches one by one, -// first imagine how DB framework accesses could be -// encapsulated into a dedicated layer. -// -]]> - UI layer shouldn't use directly DAL layer -// ND1404:UILayerShouldntUseDirectlyDALLayer - -warnif count > 0 - -// UI layer is made of types using a UI framework -let uiTypes = Application.Types.UsingAny(Assemblies.WithNameIn("PresentationFramework", "System.Windows", "System.Windows.Forms", "System.Web")) - -// Exclude commonly used DataSet and associated, from ADO.Net types -// You can easily customize this part to define what are DB types. -let dbTypes = ThirdParty.Assemblies.WithNameIn("System.Data", "EntityFramework", "NHibernate").ChildTypes() - .Except(ThirdParty.Types.WithNameIn("DataSet", "DataTable", "DataRow")) - -// DAL layer is made of types using a DB framework -// .ToHashSetEx() results to faster execution of dalTypes.Intersect(uiType.TypesIUse). -let dalTypes = Application.Types.UsingAny(dbTypes).ToHashSetEx() - -from uiType in uiTypes.UsingAny(dalTypes) -let dalTypesUsed = dalTypes.Intersect(uiType.TypesUsed) - -let dalTypesAndMembersUsed = dalTypesUsed.Union(dalTypesUsed.ChildMembers().UsedBy(uiType)) - -// Per defaut this rule estimates a technical debt -// proportional to the coupling between the UI with the DAL layer. -let couplingPerUIType = 2 + - uiType.Methods.UsingAny(dalTypesUsed).Count() + - dalTypesAndMembersUsed.Count() - -select new { - uiType, - // if dalTypesUsed is empty, it means that the uiType is part of the DAL - dalTypesAndMembersUsed, - Debt = (4 * couplingPerUIType).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is more a *sample rule to adapt to your need*, -// than a rigid rule you should abide by. It shows how to -// define code layers in a rule and how to be warned about -// layers dependencies violations. -// -// This rule first defines the UI layer and the DAL layer. -// Second it checks if any UI layer type is using directly -// any DAL layer type. -// -// • The DB layer (the DAL, **Data Access Layer**) is defined as -// the set of types of the application that use *ADO.NET*, -// *EntityFramework*, *NHibernate* types. It is easy to append -// and suppress any DB framework. -// -// • The UI layer (**User Interface Layer**) is defined as the -// set of types that use *WPF*, *Windows Form*, *ASP.NET*. -// -// *UI using directly DAL* is generally considered as *poor -// design* because DAL accesses should be a concept -// hidden to UI, encapsulated into an **intermediary domain -// logic**. -// -// Notice that per defaut this rule estimates a technical debt -// proportional to the coupling between the UI with the DAL layer. -// - -// -// This rule lists precisely which UI type uses which DAL type. -// -// More about this particular design topic here: -// http://www.kenneth-truyers.net/2013/05/12/the-n-layer-myth-and-basic-dependency-injection/ -// -]]> - Assemblies with poor cohesion (RelationalCohesion) -// ND1405:AssembliesWithPoorRelationalCohesion - -warnif count > 0 from a in Application.Assemblies - -// Build the types list on which we want to check cohesion -// This is the assembly 'a' type, minus enumeration -// and types generated by the compiler. -let types = a.ChildTypes.Where( - t => !t.IsGeneratedByCompiler && - !t.IsEnumeration && - JustMyCode.Contains(t)) - // Absolutly need ToHashet() to have fast Intersect() calls below. - .ToHashSetEx() - -// Relational Cohesion metrics is relevant only if there are enough types -where types.LongCount()> 20 && - !a.Name.EndsWith("Tests") && - !a.Name.EndsWith("Mocks") && - !a.Name.Contains("Entities") && - !a.Name.Contains("Messages") && - !a.Name.Contains("Triggers") && - !a.Name.Contains("Components") && - !a.Name.Contains("Extensions") && - !a.Name.Contains("Frameworks") && - !a.Name.Contains("Resolvers") && - !a.Name.Contains("Language") && - !a.Name.Contains("Modifiers") - -// R is the total number of relationship between types of the assemblies. -let R = types.Sum(t => t.TypesUsed.Intersect(types).Count()) - -// Relational Cohesion formula -let relationalCohesion = (double)R / types.Count -where - - (relationalCohesion < 1.5 || - relationalCohesion > 4.0) -select new { - a, - a.ChildTypes, - relationalCohesion, - a.RelationalCohesion, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This rule computes the *Relational Cohesion* metric for -// the application assemblies, and warns about wrong values. -// -// The *Relational Cohesion* for an assembly, is the total number -// of relationship between types of the assemblies, divided -// by the number of types. In other words it is the average -// number of types in the assembly used by a type in the assembly. -// -// As classes inside an assembly should be strongly related, -// the cohesion should be high. On the other hand, a value -// which is too high may indicate over-coupling. A good range -// for *Relational Cohesion* is **1.5 to 4.0**. -// -// Notice that assemblies with less than 20 types are ignored. -// - -// -// Matches of this present rule might reveal either assemblies -// with specific coding constraints (like code generated that -// have particular structure) either issues in design. -// -// In the second case, large refactoring can be planned -// not to respect this rule in particular, but to increase -// the overall design and code maintainability. -// -// The severity of issues of this rule is **Low** because -// the code metric *Relational Cohesion* is an information -// about the code structure state but **is not actionable**, -// it doesn't tell precisely what to do obtain a better score. -// -// Fixing actionable issues of others **Architecture** and -// **Code Smells** default rules will necessarily increase -// the *Relational Cohesion* scores. -//]]> - Namespaces with poor cohesion (RelationalCohesion) -// ND1406:NamespacesWithPoorRelationalCohesion - -warnif count > 0 from n in Application.Namespaces - -// Build the types list on which we want to check cohesion -// This is the namespace children types, minus enumerations -// and types generated by the compiler. -let types = n.ChildTypes.Where( - t => JustMyCode.Contains(t) && !t.IsEnumeration && - !t.Name.EndsWith("Tests") && - !t.Name.Contains("Tests+") && - !t.Name.EndsWith("Mocks") && - !t.Name.EndsWith("Extensions") && - !t.FullName.StartsWith("DeltaExtensions") && - // Exclude any exception, we might have lots of sub classes for specific exceptions - (t.BaseClass == null || - t.BaseClass.Name != "Exception" || - t.BaseClass.BaseClass != null && t.BaseClass.BaseClass.Name != "Exception") && - // Also exclude any sub classes, which are mostly exceptions too - !t.Name.Contains("+") && - !t.FullName.Contains("Entities") && - !t.FullName.Contains("Messages") && - !t.FullName.Contains("Components") && - !t.FullName.Contains("Templates") && - !t.FullName.Contains("Frameworks") && - !t.FullName.Contains("Shapes2D") && - !t.FullName.Contains("Resolvers") && - !t.FullName.Contains("WebCam")) - // Absolutly need ToHashet() to have fast Intersect() calls below. - .ToHashSetEx() - -// Relational Cohesion metrics is relevant only if there are enough types -where types.LongCount() > 20 - -// R is the total number of relationship between types of the namespaces. -let R = types.Sum(t => t.TypesUsed.Intersect(types).Count()) - -// Relational Cohesion formula -let relationalCohesion = (double)R / types.Count -where - (relationalCohesion < 1.5 || - relationalCohesion > 4.0) -select new { - n, - types, - relationalCohesion, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This rule computes the *Relational Cohesion* metric for -// the application namespaces, and warns about wrong values. -// -// The *Relational Cohesion* for a namespace, is the total number -// of relationship between types of the namespaces, divided -// by the number of types. In other words it is the average -// number of types in the namespace used by a type in the namespace. -// -// As classes inside a namespace should be strongly related, -// the cohesion should be high. On the other hand, a value -// which is too high may indicate over-coupling. A good range -// for *Relational Cohesion* is **1.5 to 4.0**. -// -// Notice that namespaces with less than 20 types are ignored. -// - -// -// Matches of this present rule might reveal either namespaces -// with specific coding constraints (like code generated that -// have particular structure) either issues in design. -// -// In the second case, refactoring sessions can be planned -// to increase the overall design and code maintainability. -// -// You can get an overview of class coupling for a -// matched namespace by exporting the *ChildTypes* to the graph. -// (Right click the *ChildTypes* cells) -// -// The severity of issues of this rule is **Low** because -// the code metric *Relational Cohesion* is an information -// about the code structure state but **is not actionable**, -// it doesn't tell precisely what to do obtain a better score. -// -// Fixing actionable issues of others **Architecture** and -// **Code Smells** default rules will necessarily increase -// the *Relational Cohesion* scores. -//]]> - Assemblies that don't satisfy the Abstractness/Instability principle -// ND1407:AssembliesThatDontSatisfyTheAbstractnessInstabilityPrinciple - -warnif count > 0 from a in Application.Assemblies - where a.NormDistFromMainSeq > 0.7 - orderby a.NormDistFromMainSeq descending -select new { - a, - a.NormDistFromMainSeq, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// The **Abstractness versus Instability Diagram** that is shown in the NDepend -// report helps to assess which assemblies are **potentially painful to maintain** -// (i.e concrete and stable) and which assemblies are **potentially useless** -// (i.e abstract and instable). -// -// • **Abstractness**: If an assembly contains many abstract types -// (i.e interfaces and abstract classes) and few concrete types, -// it is considered as abstract. -// -// • **Stability**: An assembly is considered stable if its types -// are used by a lot of types from other assemblies. In this context -// stable means *painful to modify*. -// -// From these metrics, we define the *perpendicular normalized distance of -// an assembly from the idealized line* **A + I = 1** (called *main sequence*). -// This metric is an indicator of the assembly's balance between abstractness -// and stability. We precise that the word *normalized* means that the range -// of values is [0.0 … 1.0]. -// -// This rule warns about assemblies with a *normalized distance* greater than -// than 0.7. -// -// This rules use the default code metric on assembly -// *Normalized Distance from the Main Sequence* explained here: -// https://www.ndepend.com/docs/code-metrics#DitFromMainSeq -// -// These concepts have been originally introduced by *Robert C. Martin* -// in 1994 in this paper: http://www.objectmentor.com/resources/articles/oodmetrc.pdf -// - -// -// Violations of this rule indicate assemblies with an improper -// *abstractness / stability* balance. -// -// • Either the assembly is *potentially painful to maintain* (i.e is massively -// used and contains mostly concrete types). This can be fixed by creating -// abstractions to avoid too high coupling with concrete implementations. -// -// • Either the assembly is *potentially useless* (i.e contains mostly -// abstractions and is not used enough). In such situation, the design -// must be reviewed to see if it can be enhanced. -// -// The severity of issues of this rule is **Low** because -// the *Abstractness/Instability principle* is an information -// about the code structure state but **is not actionable**, -// it doesn't tell precisely what to do obtain a better score. -// -// Fixing actionable issues of others **Architecture** and -// **Code Smells** default rules will necessarily push -// the *Abstractness/Instability principle* scores in the -// right direction. -//]]> - Higher cohesion - lower coupling -// ND1408:HigherCohesionLowerCoupling - -// warnif count > 0 -let abstractNamespaces = JustMyCode.Namespaces.Where( - n => n.ChildTypes.Where(t => !t.IsInterface && !t.IsEnumeration && !t.IsDelegate).Count() == 0 -).ToHashSetEx() - -let concreteNamespaces = JustMyCode.Namespaces.Except(abstractNamespaces).ToHashSetEx() - -from n in concreteNamespaces -let namespacesUsed = n.NamespacesUsed.ExceptThirdParty() -let concreteNamespacesUsed = namespacesUsed.Except(abstractNamespaces) -let abstractNamespacesUsed = namespacesUsed.Except(concreteNamespaces) -orderby concreteNamespacesUsed.Count() descending -select new { - n, - concreteNamespacesUsed , - abstractNamespacesUsed, - // Debt = 50.ToMinutes().ToDebt(), - // Severity = Severity.High -} - -// -// It is deemed as a good software architecture practice to clearly separate -// *abstract* namespaces that contain only abstractions (interfaces, enumerations, delegates) -// from *concrete* namespaces, that contain classes and structures. -// -// Typically, the more concrete namespaces rely on abstract namespaces *only*, -// the more **Decoupled** is the architecture, and the more **Cohesive** are -// classes inside concrete namespaces. -// -// The present code query defines sets of abstract and concrete namespaces -// and show for each concrete namespaces, which concrete and abstract namespaces -// are used. -// - -// -// This query can be transformed into a code rule, depending if you wish to -// constraint your code structure *coupling / cohesion* ratio. -//]]> - Avoid mutually-dependent types -// ND1409:AvoidMutuallyDependentTypes - -warnif count > 0 -from t1 in Application.Types -where t1.NbLinesOfCode > 10 && - (t1.IsClass || t1.IsStructure) - -from t2 in t1.TypesUsingMe -where t2.NbLinesOfCode > 10 && - (t2.IsClass || t2.IsStructure) && - t1.IsUsing(t2) - -// To avoid reporting twice the same pair -// use lexicographically sort of full names of types -// to only take the first one. -where new string[] {t1.FullName, t2.FullName}.OrderBy(x => x).First() == t1.FullName - -// So far we only keep dependencies that involve method call. -// It is possible to comment the two 'where' clauses below to get also -// others kind of dependencies, like usage of typeof(Class). -let t2MethodsUsingT1 = t2.Methods.Where(m => m.IsUsing(t1)).ToArray() -where t2MethodsUsingT1.Length > 0 - -let t1MethodsUsingT2 = t1.Methods.Where(m => m.IsUsing(t2)).ToArray() -where t1MethodsUsingT2.Length > 0 - -select new { - t1, t2, - t2MethodsUsingT1, - t1MethodsUsingT2, - Debt = 20.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule matches pairs of classes or structures that use each others. -// -// In this situation both types are not isolated: each type cannot be -// reviewed, refactored or tested without the other one. -// -// This rule is disabled by default because there are some common -// situations, like when implementing recursive data structures, -// complex serialization or using the visitor pattern, where having -// mutually dependent types is legitimate. -// Just enable this rule if you wish to fix most of its issues. -// - -// -// Fixing mutually-dependent types means identifying the unwanted dependency -// (*t1* using *t2*, or *t2* using *t1*) and then removing this dependency. -// -// Often you'll have to use the **dependency inversion principle** by creating -// one or several interfaces dedicated to abstract one implementation -// from the other one: https://en.wikipedia.org/wiki/Dependency_inversion_principle -//]]> - Example of custom rule to check for dependency -// ND9900:ExplicitId9900 - -warnif count > 0 from a in Assemblies -where -a.IsUsing("Foo1.Foo2".AllowNoMatch().MatchNamespace()) && -(a.Name == @"Foo3") -select new { - a, - a.NbLinesOfCode, - // Debt and Severity / Annual Interest can be modified to estimate well the - // effort to fix the issue and the annual cost to leave the issue unfixed. - Debt = 30.ToMinutes().ToDebt(), - Severity = Severity.High -} -// the assembly Foo3 -// shouldn't use directly -// the namespace Foo3.Foo4 -// because (insert your reason) - -// -// This rule is a **sample rule** that shows how to -// check if a particular dependency exists or not, -// from a code element **A** to a code element **B**, -// **A** and **B** being an *Assembly*, a *Namespace*, a *Type*, -// a *Method* or a *Field*, **A** and **B** being not -// necessarily of same kind (i.e two Assemblies or -// two Namespaces…). -// -// Such rule can be generated: -// -// • by right clicking the cell in the *Dependency Matrix* -// with **B** in row and **A** in column, -// -// • or by right-clicking the concerned arrow in the *Dependency -// Graph* from **A** to **B**, -// -// and in both cases, click the menu -// **Generate a code rule that warns if this dependency exists** -// -// The generated rule will look like this one. -// It is now up to you to adapt this rule to check exactly -// your needs. -// - -// -// This is a *sample rule* there is nothing to fix *as is*. -//]]> - - - API Breaking Changes: Types -// ND1500:APIBreakingChangesTypes - -warnif count > 0 from t in codeBase.OlderVersion().Application.Types -where t.IsPubliclyVisible && - - // The type has been removed, it was not tagged as obsolete - // and its parent assembly hasn't been removed … - ( ( t.WasRemoved() && - !t.ParentAssembly.WasRemoved() && - !t.IsObsolete) || - - // … or the type is not publicly visible anymore - !t.WasRemoved() && !t.NewerVersion().IsPubliclyVisible) - -select new { - t, - NewVisibility = - (t.WasRemoved() ? " " : - t.NewerVersion().Visibility.ToString()), - Debt = 20.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule warns if a type publicly visible in the *baseline*, -// is not publicly visible anymore or if it has been removed. -// Clients code using such type will be broken. -// - -// -// Make sure that public types that used to be presented to -// clients, still remain public now, and in the future. -// -// If a public type must really be removed, you can tag it -// with *System.ObsoleteAttribute* with a *workaround message* -// during a few public releases, until it gets removed definitely. -// Notice that this rule doesn't match types removed that were -// tagged as obsolete. -// -// Issues of this rule have a severity equal to **High** -// because an API Breaking change can provoque significant -// friction with consumers of the API. -//]]> - API Breaking Changes: Methods -// ND1501:APIBreakingChangesMethods - -warnif count > 0 from m in codeBase.OlderVersion().Application.Methods -where m.IsPubliclyVisible && - - // The method has been removed, it was not tagged as obsolete - // and its parent type hasn't been removed … - ( ( m.WasRemoved() && - !m.ParentType.WasRemoved() && - !m.IsObsolete ) - - // … or the method is not publicly visible anymore - || (!m.WasRemoved() && !m.NewerVersion().IsPubliclyVisible) - - // … or the method return type has changed - || (!m.WasRemoved() && m.ReturnType != null && m.NewerVersion().ReturnType != null - && m.ReturnType.FullName != m.NewerVersion().ReturnType.FullName) - ) - -//-------------------------------------- -// Handle special case: if between two versions a regular property becomes -// an auto-property (or vice-versa) the property getter/setter method have -// a different value for IMethod.IsGeneratedByCompiler -// since auto-property getter/setter are marked as generated by the compiler. -// -// If a method IsGeneratedByCompiler value changes between two versions, -// NDepend doesn't pair the newer/older occurences of the method. -// -// Hence in such situation, a public method is seen as added -// and a public method is seen as removed, but the API is not broken! -// The equivalentMethod-check below avoids reporting such -// API Breaking Change false-positive. -let equivalentMethod = m.WasRemoved() && m.ParentType.IsPresentInBothBuilds() ? - m.ParentType.NewerVersion().Methods - .FirstOrDefault(m1 => - m1.IsPubliclyVisible && - m1.Name == m.Name && - m1.IsGeneratedByCompiler != m.IsGeneratedByCompiler && - (m1.ReturnType == null || m.ReturnType == null || m1.ReturnType.FullName == m.ReturnType.FullName) - ) - : null -where equivalentMethod == null -//-------------------------------------- - - -select new { - m, - NewVisibility = - (m.WasRemoved() ? " " : - m.NewerVersion().Visibility.ToString()), - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule warns if a method publicly visible in the *baseline*, -// is not publicly visible anymore or if it has been removed. -// Clients code using such method will be broken. -// - -// -// Make sure that public methods that used to be presented to -// clients, still remain public now, and in the future. -// -// If a public method must really be removed, you can tag it -// with *System.ObsoleteAttribute* with a *workaround message* -// during a few public releases, until it gets removed definitely. -// Notice that this rule doesn't match methods removed that were -// tagged as obsolete. -// -// Issues of this rule have a severity equal to **High** -// because an API Breaking change can provoque significant -// friction with consumers of the API. -// -]]> - API Breaking Changes: Fields -// ND1502:APIBreakingChangesFields - -warnif count > 0 from f in codeBase.OlderVersion().Application.Fields -where f.IsPubliclyVisible && - - // The field has been removed, it was not tagged as obsolete - // and its parent type hasn't been removed … - ( ( f.WasRemoved() && - !f.ParentType.WasRemoved() && - !f.IsObsolete) - - // … or the field is not publicly visible anymore - || !f.WasRemoved() && !f.NewerVersion().IsPubliclyVisible) - - // … or the field type has changed - || (!f.WasRemoved() && f.FieldType != null && f.NewerVersion().FieldType != null - && f.FieldType.FullName != f.NewerVersion().FieldType.FullName) - -select new { - f, - NewVisibility = - (f.WasRemoved() ? " " : - f.NewerVersion().Visibility.ToString()), - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule warns if a field publicly visible in the *baseline*, -// is not publicly visible anymore or if it has been removed. -// Clients code using such field will be broken. -// - -// -// Make sure that public fields that used to be presented to -// clients, still remain public now, and in the future. -// -// If a public field must really be removed, you can tag it -// with *System.ObsoleteAttribute* with a *workaround message* -// during a few public releases, until it gets removed definitely. -// Notice that this rule doesn't match fields removed that were -// tagged as obsolete. -// -// Issues of this rule have a severity equal to **High** -// because an API Breaking change can provoque significant -// friction with consumers of the API. -//]]> - API Breaking Changes: Interfaces and Abstract Classes -// ND1503:APIBreakingChangesInterfacesAndAbstractClasses - -warnif count > 0 from tNewer in Application.Types where - (tNewer.IsInterface || tNewer.IsClass && tNewer.IsAbstract) && - tNewer.IsPubliclyVisible && - tNewer.IsPresentInBothBuilds() - -let tOlder = tNewer.OlderVersion() where tOlder.IsPubliclyVisible - -let methodsRemoved = tOlder.Methods.Where(m => m.IsAbstract && m.WasRemoved()) -let methodsAdded = tNewer.Methods.Where(m => m.IsAbstract && m.WasAdded()) - -where methodsAdded.Count() > 0 || methodsRemoved.Count() > 0 -select new { - tNewer, - methodsAdded, - methodsRemoved, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule warns if a publicly visible interface or abstract class -// has been changed and contains new abstract methods or -// if some abstract methods have been removed. -// -// Clients code that implement such interface or derive from -// such abstract class will be broken. -// - -// -// Make sure that the public contracts of interfaces and abstract classes -// that used to be presented to clients, remain stable now, and in the future. -// -// If a public contract must really be changed, you can tag -// abstract methods that will be removed with *System.ObsoleteAttribute* -// with a *workaround message* during a few public releases, until it gets -// removed definitely. -// -// Issues of this rule have a severity equal to **High** -// because an API Breaking change can provoque significant -// friction with consummers of the API. -// The severity is not set to **Critical** because an interface -// is not necessarily meant to be implemented by the consummer -// of the API. -//]]> - Broken serializable types -// ND1504:BrokenSerializableTypes - -warnif count > 0 - -from t in Application.Types where - - // Collect types tagged with SerializableAttribute - t.HasAttribute("System.SerializableAttribute".AllowNoMatch()) && - !t.IsDelegate && - t.IsPresentInBothBuilds() && - t.HasAttribute(t) - - // Find newer and older versions of NonSerializedAttribute - let newNonSerializedAttribute = ThirdParty.Types.WithFullName("System.NonSerializedAttribute").SingleOrDefault() - let oldNonSerializedAttribute = newNonSerializedAttribute == null ? null : newNonSerializedAttribute.OlderVersion() - - // Find added or removed fields not marked with NonSerializedAttribute - let addedInstanceField = from f in t.InstanceFields where - f.WasAdded() && - (newNonSerializedAttribute == null || !f.HasAttribute(newNonSerializedAttribute)) - select f - let removedInstanceField = from f in t.OlderVersion().InstanceFields where - f.WasRemoved() && - (oldNonSerializedAttribute == null || !f.HasAttribute(oldNonSerializedAttribute)) - select f - where addedInstanceField.Count() > 0 || removedInstanceField.Count() > 0 - -select new { - t, - addedInstanceField, - removedInstanceField, - Debt = 20.ToMinutes().ToDebt(), - Severity = Severity.Critical -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule warns about breaking changes in types tagged with -// *SerializableAttribute*. -// -// To do so, this rule searches for serializable type with serializable -// instance fields added or removed. Notice that it doesn't take account -// of fields tagged with *NonSerializedAttribute*. -// -// From http://msdn.microsoft.com/library/system.serializableattribute.aspx : -// "All the public and private fields in a type that are marked by the -// *SerializableAttribute* are serialized by default, unless the type -// implements the *ISerializable* interface to override the serialization process. -// The default serialization process excludes fields that are marked -// with the *NonSerializedAttribute* attribute." -// - -// -// Make sure that the serialization process of serializable types remains -// stable now, and in the future. -// -// Else you'll have to deal with **Version Tolerant Serialization** -// that is explained here: -// https://msdn.microsoft.com/en-us/library/ms229752(v=vs.110).aspx -// -// Issues of this rule have a severity equal to **High** -// because an API Breaking change can provoque significant -// friction with consummers of the API. -//]]> - Avoid changing enumerations Flags status -// ND1505:AvoidChangingEnumerationsFlagsStatus - -warnif count > 0 - -let oldFlags = codeBase.OlderVersion().ThirdParty.Types.WithFullName("System.FlagsAttribute").FirstOrDefault() -let newFlags = ThirdParty.Types.WithFullName("System.FlagsAttribute").FirstOrDefault() -where oldFlags != null && newFlags != null - -from t in Application.Types where - t.IsEnumeration && - t.IsPresentInBothBuilds() -let hasFlagsAttributeNow = t.HasAttribute(newFlags) -let usedToHaveFlagsAttribute = t.OlderVersion().HasAttribute(oldFlags) -where hasFlagsAttributeNow != usedToHaveFlagsAttribute -select new { - t, - hasFlagsAttributeNow, - usedToHaveFlagsAttribute, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule matches enumeration types that used to be tagged -// with *FlagsAttribute* in the *baseline*, and not anymore. -// It also matches the opposite, enumeration types that are now -// tagged with *FlagsAttribute*, and were not tagged in the *baseline*. -// -// Being tagged with *FlagsAttribute* is a strong property for an enumeration. -// Not so much in terms of *behavior* (only the *enum.ToString()* method -// behavior changes when an enumeration is tagged with *FlagsAttribute*) -// but in terms of *meaning*: is the enumeration a **range of values** -// or a **range of flags**? -// -// As a consequence, changing the *FlagsAttribute*s status of an enumeration can -// have significant impact for its clients. -// - -// -// Make sure the *FlagsAttribute* status of each enumeration remains stable -// now, and in the future. -//]]> - API: New publicly visible types -from t in Application.Types -where t.IsPubliclyVisible && - - // The type has been removed and its parent assembly hasn't been removed … - ( (t.WasAdded() && !t.ParentAssembly.WasAdded()) || - - // … or the type existed but was not publicly visible - !t.WasAdded() && !t.OlderVersion().IsPubliclyVisible) - -select new { - t, - OldVisibility = - (t.WasAdded() ? " " : - t.OlderVersion().Visibility.ToString()), -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists types that are new in the public -// surface of the analyzed assemblies. -//]]> - API: New publicly visible methods -from m in Application.Methods -where m.IsPubliclyVisible && - - // The method has been removed and its parent assembly hasn't been removed … - ( (m.WasAdded() && !m.ParentType.WasAdded()) || - - // … or the method existed but was not publicly visible - !m.WasAdded() && !m.OlderVersion().IsPubliclyVisible) - -//-------------------------------------- -// Handle special case: if between two versions a regular property becomes -// an auto-property (or vice-versa) the property getter/setter method have -// a different value for IMethod.IsGeneratedByCompiler -// since auto-property getter/setter are marked as generated by the compiler. -// -// If a method IsGeneratedByCompiler value changes between two versions, -// NDepend doesn't pair the newer/older occurences of the method. -// -// Hence in such situation, a public method is seen as added -// and a public method is seen as removed, but the API is not broken! -// The equivalentMethod-check below avoids reporting such -// API Breaking Change false-positive. -let equivalentMethod = m.WasAdded() && m.ParentType.IsPresentInBothBuilds() ? - m.ParentType.OlderVersion().Methods - .FirstOrDefault(m1 => - m1.IsPubliclyVisible && - m1.Name == m.Name && - m1.IsGeneratedByCompiler != m.IsGeneratedByCompiler) - : null -where equivalentMethod == null -//-------------------------------------- - -select new { - m, - OldVisibility = - (m.WasAdded() ? " " : - m.OlderVersion().Visibility.ToString()) -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists methods that are new in the public -// surface of the analyzed assemblies. -// -]]> - API: New publicly visible fields -from f in Application.Fields -where f.IsPubliclyVisible && - - // The method has been removed and its parent assembly hasn'f been removed … - ( (f.WasAdded() && !f.ParentType.WasAdded()) || - - // … or the t existed but was not publicly visible - !f.WasAdded() && !f.OlderVersion().IsPubliclyVisible) - -select new { - f, - OldVisibility = - (f.WasAdded() ? " " : - f.OlderVersion().Visibility.ToString()) -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists fields that are new in the public -// surface of the analyzed assemblies. -//]]> - - - Code should be tested -// ND1600:CodeShouldBeTested - -warnif count > 0 - -// This lambda infers a factor in the range [0,1] from a sequence of distinct types used, -// based on how many of these types are actually concrete (i.e are not interfaces or enumeration). -// Primitive types (int, bool...) are eliminated from the sequence of types used -// by filtering types declared in the namespace System. -let abstractionUsageFactorFormula = new Func,double>(typesUsed => - typesUsed.Count(t => t.ParentNamespace.Name != "System" && !t.IsInterface && !t.IsEnumeration) - / (1 + typesUsed.Count(t => t.ParentNamespace.Name != "System"))) - - -from method in Application.Methods - where !method.IsExcludedFromCoverage && method.NbLinesOfCode >= 0 && method.PercentageCoverage < 100 - - // Factor in case method is partially covered - let uncoverageFactor = ((100 - method.PercentageCoverage) / 100).Value - - // Complexity factor - let complexityFactor = ((method.CyclomaticComplexity ?? method.ILCyclomaticComplexity) + method.ILNestingDepth).Linear(0, 0.1, 10, 1).Value - - // Not my code is often generated code and is in general easier to get tested since test can be generated as well. - let justMyCodeFactor = (JustMyCode.Contains(method) ? 1 : 0.4) - - // abstractionUsageFactor reflects the fact that code that relies on interfaces - // is easier to test that code that relies on concrete classes. - let abstractionUsageFactor = 0.7 + 0.3 *abstractionUsageFactorFormula(method.MembersUsed.Select(m => m.ParentType).Distinct()) - - // The usageFactor depends on the method 'rank' that is a value - // indicating if the method is often used or not - let usageFactor = (method.Rank / (method.Rank + 4)).Value - - // It is more complicated to write tests for non publicly visible methods - let visibilityFactor = method.Visibility.EqualsAny(Visibility.Public, Visibility.Internal) ? 1 : - method.Visibility != Visibility.Private ? 1.1 : 1.2 - - // Is is more complicated to write tests for methods that read mutable static fields - // whose changing state is shared across tests executions. - let staticFieldUsageFactor = method.ReadsMutableTypeState ? 1.3 : 1.0 - - - // Both "effort to write tests" and "annual cost to not test" for a method - // is determined by several factors in the range [0,1] that multiplies the effortToDevelop - let effortToDevelopInMinutes = method.EffortToDevelop().Value.TotalMinutes - - let effortToWriteTests = Math.Max(2, // Minimum 2 minutes per method not tested - effortToDevelopInMinutes * - uncoverageFactor * - complexityFactor * - justMyCodeFactor * - abstractionUsageFactor * - visibilityFactor * - staticFieldUsageFactor).ToMinutes().ToDebt() - - let annualCostToNotFix = Math.Max(2, // Minimum 2 minutes per method not tested - effortToDevelopInMinutes * - usageFactor * - uncoverageFactor * - justMyCodeFactor).ToMinutes().ToAnnualInterest() - - orderby annualCostToNotFix.Value descending - -select new { - method, - method.PercentageCoverage, - method.NbLinesOfCode, - method.NbLinesOfCodeNotCovered, - method.CyclomaticComplexity, - Debt = effortToWriteTests, - AnnualInterest = annualCostToNotFix, - - // BreakingPoint = effortToWriteTests.BreakingPoint(annualCostToNotFix), - - // Uncomment the line below to tinker with various factors - // uncoverageFactor, complexityFactor , justMyCodeFactor , abstractionUsageFactor, visibilityFactor, staticFieldUsageFactor -} - - -// -// This rule lists methods not covered at all by test -// or partially covered by tests. -// -// For each match, the rules estimates the **technical debt**, i.e -// the effort to write unit and integration tests for the method. -// The estimation is based on the effort to develop the code element -// multiplied by factors in the range ]0,1.3] based on -// -// • the method code size and complexity -// -// • the actual percentage coverage -// -// • the abstractness of types used, because relying on classes instead of -// interfaces makes the code more difficult to test -// -// • the method visibility because testing private or protected -// methods is more difficult than testing public and internal ones -// -// • the fields used by the method, because is is more complicated to -// write tests for methods that read mutable static fields whose changing -// state is shared across tests executions. -// -// • whether the method is considered *JustMyCode* or not because *NotMyCode* -// is often generated easier to get tested since tests can be generated as well. -// -// This rule is necessarily a large source of technical debt, since -// the code left untested is by definition part of the technical debt. -// -// This rule also estimates the **annual interest**, i.e the annual cost -// to let the code uncovered, based on the effort to develop the -// code element, multiplied by factors based on usage of the code element. -// - -// -// Write unit tests to test and cover the methods and their parent classes -// matched by this rule. -//]]> - New Methods should be tested -// ND1601:NewMethodsShouldBeTested - -warnif count > 0 -from m in Application.Methods where - m.NbLinesOfCode > 0 && - m.PercentageCoverage < 30 && - m.WasAdded() - orderby m.NbLinesOfCode descending, - m.NbLinesOfCodeNotCovered , - m.PercentageCoverage -select new { - m, - m.PercentageCoverage, - m.NbLinesOfCode, - m.NbLinesOfCodeNotCovered, - - // Simplistic Debt estimation, because the effort to write tests for a method not 100% tested - // is already estimated properly with the rule "Code should be tested". - Debt = m.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(), - - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// This rule operates only on methods added or refactored since the baseline. -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// It is important to write code mostly covered by tests -// to achieve *maintainable* and *non-error-prone* code. -// -// In real-world, many code bases are poorly covered by tests. -// However it is not practicable to stop the development for months -// to refactor and write tests to achieve high code coverage ratio. -// -// Hence it is recommended that each time a method (or a type) gets added, -// the developer takes the time to write associated unit-tests to cover it. -// -// Doing so will help to increase significantly the maintainability of the code base. -// You'll notice that quickly, refactoring will also be driven by testability, -// and as a consequence, the overall code structure and design will increase as well. -// -// Issues of this rule have a **High** severity because they reflect -// an actual trend to not care about writing tests on refactored code. -// - -// -// Write unit-tests to cover the code of most methods and classes added. -//]]> - Methods refactored should be tested -// ND1602:MethodsRefactoredShouldBeTested - -warnif count > 0 -from m in Application.Methods where - m.PercentageCoverage < 30 && - m.CodeWasChanged() - orderby m.NbLinesOfCode descending, - m.NbLinesOfCodeNotCovered , - m.PercentageCoverage -select new { - m, - m.PercentageCoverage, - m.NbLinesOfCode, - m.NbLinesOfCodeNotCovered, - - // Simplistic Debt estimation, because the effort to write tests for a method not 100% tested - // is already estimated properly with the rule "Code should be tested". - Debt = m.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(), - - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// This rule operates only on methods added or refactored since the baseline. -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// It is important to write code mostly covered by tests -// to achieve *maintainable* and *non-error-prone* code. -// -// In real-world, many code bases are poorly covered by tests. -// However it is not practicable to stop the development for months -// to refactor and write tests to achieve high code coverage ratio. -// -// Hence it is recommended that each time a method (or a type) gets refactored, -// the developer takes the time to write associated unit-tests to cover it. -// -// Doing so will help to increase significantly the maintainability of the code base. -// You'll notice that quickly, refactoring will also be driven by testability, -// and as a consequence, the overall code structure and design will increase as well. -// -// Issues of this rule have a **High** severity because they reflect -// an actual trend to not care about writing tests on refactored code. -// - -// -// Write unit-tests to cover the code of most methods and classes refactored. -//]]> - Assemblies Namespaces and Types should be tested -// ND1603:AssembliesNamespacesAndTypesShouldBeTested - -warnif count > 0 -from elem in CodeElementParents -where elem.NbLinesOfCode > 0 && - elem.PercentageCoverage == 0 && - elem.Parent.PercentageCoverage != 0 -orderby elem.NbLinesOfCode descending -select new { - elem, - elem.NbLinesOfCodeNotCovered, - Debt = 4.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This rule lists assemblies, namespaces and types that are not -// covered at all by unit tests. -// -// If a parent is matched its children are not matched. For example -// if a namespace is matched, its child types are not matched. -// -// This rule goal is not to collide with the **Code should be tested** -// rule that lists uncovered code for each method and infer the effort -// to write unit tests (the *Debt*) and the annual cost to let the code -// untested (the Annual Interest). -// -// This rule goal is to inform of large code elements left untested. -// As a consequence the *Debt* per issue is only 4 minutes and the -// severity of the issues is *Low*. -// -// -// -// Write unit and integration tests to cover, even partially, -// code elements matched by this rule. -// -// Then use issues of the rules **Code should be tested**, -// **New Methods should be tested** and -// **Methods refactored should be tested** -// to write more tests where it matters most, and eventually -// refactor some code to make it more testable. -//]]> - Types almost 100% tested should be 100% tested -// ND1604:TypesAlmost100PercentTestedShouldBe100PercentTested - -warnif count > 0 -from t in Application.Types where - t.PercentageCoverage >= 95 && - t.PercentageCoverage <= 99 && - !t.IsGeneratedByCompiler - - let methodsCulprit = t.Methods.Where(m => m.PercentageCoverage < 100) - - orderby t.NbLinesOfCode descending , - t.NbLinesOfCodeNotCovered , - t.PercentageCoverage -select new { - t, - t.PercentageCoverage, - t.NbLinesOfCode, - t.NbLinesOfCodeNotCovered, - methodsCulprit, - - // Simplistic Debt estimation, because the effort to write tests for a type not 100% tested - // is already estimated properly with the rule "Code should be tested". - Debt = t.NbLinesOfCodeNotCovered.Linear(1,2, 20,20).ToMinutes().ToDebt(), - - Severity = Severity.High -} - -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// Often covering the few percents of remaining uncovered code of a class, -// requires as much work as covering the first 90%. -// For this reason, often teams estimate that 90% coverage is enough. -// However *untestable code* usually means *poorly written code* -// which usually leads to *error prone code*. -// So it might be worth refactoring and making sure to cover the few uncovered lines of code -// **because most tricky bugs might come from this small portion of hard-to-test code**. -// -// Not all classes should be 100% covered by tests (like UI code can be hard to test) -// but you should make sure that most of the logic of your application -// is defined in some *easy-to-test classes*, 100% covered by tests. -// -// Issues of this rule have a **High** severity because as explained, -// such situation is *bug-prone*. -// - -// -// Write more unit-tests dedicated to cover code not covered yet. -// If you find some *hard-to-test code*, it is certainly a sign that this code -// is not *well designed* and hence, needs refactoring. -//]]> - Namespaces almost 100% tested should be 100% tested -// ND1605:NamespacesAlmost100PercentTestedShouldBe100PercentTested - -warnif count > 0 -from n in Application.Namespaces where - n.PercentageCoverage >= 95 && - n.PercentageCoverage <= 99 - - let methodsCulprit = n.ChildMethods.Where(m => m.PercentageCoverage < 100) - - orderby n.NbLinesOfCode descending , - n.NbLinesOfCodeNotCovered , - n.PercentageCoverage -select new { - n, - n.PercentageCoverage, - n.NbLinesOfCode, - n.NbLinesOfCodeNotCovered, - methodsCulprit, - - // Simplistic Debt estimation, because the effort to write tests for a type not 100% tested - // is already estimated properly with the rule "Code should be tested". - Debt = n.NbLinesOfCodeNotCovered.Linear(1,2, 50,60).ToMinutes().ToDebt(), - - Severity = Severity.High -} - -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// Often covering the few percents of remaining uncovered code of -// one or several classes in a namespace -// requires as much work as covering the first 90%. -// For this reason, often teams estimate that 90% coverage is enough. -// However *untestable code* usually means *poorly written code* -// which usually leads to *error prone code*. -// So it might be worth refactoring and making sure to cover the few uncovered lines of code -// **because most tricky bugs might come from this small portion of hard-to-test code**. -// -// Not all classes should be 100% covered by tests (like UI code can be hard to test) -// but you should make sure that most of the logic of your application -// is defined in some *easy-to-test classes*, 100% covered by tests. -// -// Issues of this rule have a **High** severity because as explained, -// such situation is *bug-prone*. -// - -// -// Write more unit-tests dedicated to cover code not covered yet in the namespace. -// If you find some *hard-to-test code*, it is certainly a sign that this code -// is not *well designed* and hence, needs refactoring. -//]]> - Types that used to be 100% covered by tests should still be 100% covered -// ND1606:TypesThatUsedToBe100PercentCoveredByTestsShouldStillBe100PercentCovered - -warnif count > 0 -from t in JustMyCode.Types where - t.IsPresentInBothBuilds() && - t.OlderVersion().PercentageCoverage == 100 && - t.PercentageCoverage < 100 - -from m in t .MethodsAndConstructors where - m.NbLinesOfCode> 0 && - m.PercentageCoverage < 100 && - !m.IsExcludedFromCoverage - -select new { - m, - m.PercentageCoverage, - - // Simplistic Debt estimation, because the effort to write tests for a method not 100% tested - // is already estimated properly with the rule "Code should be tested". - Debt = t.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(), - - Severity = Severity.High -} - -// -// This rule is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// Often covering 10% of remaining uncovered code of a class, -// requires as much work as covering the first 90%. -// For this reason, typically teams estimate that 90% coverage is enough. -// However *untestable code* usually means *poorly written code* -// which usually leads to *error prone code*. -// So it might be worth refactoring and making sure to cover the 10% remaining code -// **because most tricky bugs might come from this small portion of hard-to-test code**. -// -// Not all classes should be 100% covered by tests (like UI code can be hard to test) -// but you should make sure that most of the logic of your application -// is defined in some *easy-to-test classes*, 100% covered by tests. -// -// In this context, this rule warns when a type fully covered by tests is now only partially covered. -// -// Issues of this rule have a **High** severity because often, -// a type that used to be 100% and is not covered anymore -// is a bug-prone situation that should be carefully handled. -// - -// -// Write more unit-tests dedicated to cover code not covered anymore. -// If you find some *hard-to-test code*, it is certainly a sign that this code -// is not *well designed* and hence, needs refactoring. -// -// You'll find code impossible to cover by unit-tests, like calls to *MessageBox.Show()*. -// An infrastructure must be defined to be able to *mock* such code at test-time. -//]]> - Types tagged with FullCoveredAttribute should be 100% covered -// ND1607:TypesTaggedWithFullCoveredAttributeShouldBe100PercentCovered - -warnif count > 0 -from t in Application.Types where - t.HasAttribute ("NDepend.Attributes.FullCoveredAttribute".AllowNoMatch()) && - t.PercentageCoverage < 100 - -from m in t .MethodsAndConstructors where - m.NbLinesOfCode> 0 && - m.PercentageCoverage < 100 && - !m.IsExcludedFromCoverage - -select new { - m, - m.PercentageCoverage, - m.NbLinesOfCodeNotCovered, - m.NbLinesOfCode, - - // Simplistic Debt estimation, because the effort to write tests for a method not 100% tested - // is already estimated properly with the rule "Code should be tested". - Debt = m.NbLinesOfCodeNotCovered.Linear(1,2, 10,10).ToMinutes().ToDebt(), - - Severity = Severity.High -} - -// -// This rule lists methods partially covered by tests, of types tagged with -// **FullCoveredAttribute**. -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// By using a **FullCoveredAttribute**, you can express in source code the intention -// that a class is 100% covered by tests, and should remain 100% covered in the future. -// If you don't want to link *NDepend.API.dll*, -// you can use your own attribute and adapt the source code of this rule. -// -// Benefits of using a **FullCoveredAttribute** are twofold: -// Not only the intention is expressed in source code, -// but it is also continuously checked by the present rule. -// -// Often covering 10% of remaining uncovered code of a class, -// requires as much work as covering the first 90%. -// For this reason, often teams estimate that 90% coverage is enough. -// However *untestable code* usually means *poorly written code* which usually means *error prone code*. -// So it might be worth refactoring and making sure to cover the 10% remaining code -// **because most tricky bugs might come from this small portion of hard-to-test code**. -// -// Not all classes should be 100% covered by tests (like UI code can be hard to test) -// but you should make sure that most of the logic of your application -// is defined in some *easy-to-test classes*, 100% covered by tests. -// -// Issues of this rule have a **High** severity because often, -// a type that used to be 100% and is not covered anymore -// is a bug-prone situation that should be carefully handled. -// - -// -// Write more unit-tests dedicated to cover code of matched classes not covered yet. -// If you find some *hard-to-test code*, it is certainly a sign that this code -// is not *well designed* and hence, needs refactoring. -//]]> - Types 100% covered should be tagged with FullCoveredAttribute -// ND1608:Types100PercentCoveredShouldBeTaggedWithFullCoveredAttribute - -warnif count > 0 from t in JustMyCode.Types where - !t.HasAttribute ("NDepend.Attributes.FullCoveredAttribute".AllowNoMatch()) && - t.PercentageCoverage == 100 && - !t.IsGeneratedByCompiler -select new { - t, - t.NbLinesOfCode, - Debt = 3.ToMinutes().ToDebt(), // It is fast to add such attribute to a type. - Severity = Severity.Low -} - -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// By using a **FullCoveredAttribute**, you can express in source code the intention -// that a class is 100% covered by tests, and should remain 100% covered in the future. -// -// Benefits of using a **FullCoveredAttribute** are twofold: -// Not only the intention is expressed in source code, -// but it is also continuously checked by the present rule. -// -// Issues of this rule have an **Low** severity because they don't reflect -// a problem, but provide an advice for potential improvement. -// - -// -// Just tag types 100% covered by tests with the **FullCoveredAttribute** -// that can be found in *NDepend.API.dll*, -// or by an attribute of yours defined in your own code -// (in which case this rule must be adapted). -//]]> - Methods should have a low C.R.A.P score -// ND1609:MethodsShouldHaveALowCRAPScore - -warnif count > 0 -from m in JustMyCode.Methods - -// Don't match too short methods -where m.NbLinesOfCode > 10 && m.CoverageDataAvailable - -let CC = m.CyclomaticComplexity -let uncov = (100 - m.PercentageCoverage) / 100f -let CRAP = (double)(CC * CC * uncov * uncov * uncov) + CC -where CRAP != null && CRAP > 30 -orderby CRAP descending, m.NbLinesOfCode descending -select new { - m, - CRAP, - CC, - m.PercentageCoverage, m.NbLinesOfCode, - - // CRAP score equals 30 => 10 minutes debt - // CRAP score equals 3000 => 3 hours to write tests - Debt = CRAP.Linear(30,10, 3000, 3*60).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is executed only if some code coverage data is imported -// from some code coverage files. -// -// So far this rule is disabled because other code coverage rules -// assess properly code coverage issues. -// -// **Change Risk Analyzer and Predictor** (i.e. CRAP) is a code metric -// that helps in pinpointing overly both complex and untested code. -// Is has been first defined here: -// http://www.artima.com/weblogs/viewpost.jsp?thread=215899 -// -// The Formula is: **CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m)** -// -// • where *CC(m)* is the *cyclomatic complexity* of the method *m* -// -// • and *cov(m)* is the *percentage coverage* by tests of the method *m* -// -// Matched methods cumulates two highly *error prone* code smells: -// -// • A complex method, difficult to develop and maintain. -// -// • Non 100% covered code, difficult to refactor without introducing any regression bug. -// -// The higher the CRAP score, the more painful to maintain and error prone is the method. -// -// An arbitrary threshold of 30 is fixed for this code rule as suggested by inventors. -// -// Notice that no amount of testing will keep methods with a Cyclomatic Complexity -// higher than 30, out of CRAP territory. -// -// Notice that this rule doesn't match too short method -// with less than 10 lines of code. -// - -// -// In such situation, it is recommended to both refactor the complex method logic -// into several smaller and less complex methods -// (that might belong to some new types especially created), -// and also write unit-tests to full cover the refactored logic. -// -// You'll find code impossible to cover by unit-tests, like calls to *MessageBox.Show()*. -// An infrastructure must be defined to be able to *mock* such code at test-time. -//]]> - Test Methods - -let testAttr = ThirdParty.Types.WithNameIn("FactAttribute", "TestAttribute", "TestCaseAttribute") -let testMethods = Methods.TaggedWithAnyAttributes(testAttr) -from m in testMethods -select m - -// -// We advise to not include test assemblies in code analyzed by NDepend. -// We estimate that it is acceptable and practical to lower the quality gate of test code, -// because the important measures for tests are: -// -// • The coverage ratio, -// -// • And the amount of logic results asserted: This includes both -// assertions in test code, and assertions in code covered by tests, -// like *Code Contract* assertions and *Debug.Assert(…)* assertions. -// -// But if you wish to enforce the quality of test code, you'll need to -// consider test assemblies in your list of application assemblies -// analyzed by NDepend. -// -// In such situation, this code query lists tests methods and you can -// reuse this code in custom rules. -//]]> - Methods directly called by test Methods - -let testAttr = ThirdParty.Types.WithNameIn("FactAttribute", "TestAttribute", "TestCaseAttribute") -let testMethods = Methods.TaggedWithAnyAttributes(testAttr).ToHashSetEx() - -// --- Uncomment this line if your test methods are in dedicated test assemblies --- -//let testAssemblies = testMethods.ParentAssemblies().ToHashSetEx() - -from m in Application.Methods.UsedByAny(testMethods) - -// --- Uncomment this line if your test methods are in dedicated test assemblies --- -//where !testAssemblies.Contains(m.ParentAssembly) - -select new { m , - calledByTests = m.MethodsCallingMe.Intersect(testMethods ), - // --- Uncomment this line if your project import some coverage data --- - // m.PercentageCoverage -} - - -// -// This query lists all methods directly called by tests methods. -// Overrides of virtual and abstract methods, called through polymorphism, are not listed. -// Methods solely invoked through a delegate are not listed. -// Methods solely invoked through reflection are not listed. -// -// We advise to not include test assemblies in code analyzed by NDepend. -// We estimate that it is acceptable and practical to lower the quality gate of test code, -// because the important measures for tests are: -// -// • The coverage ratio, -// -// • And the amount of logic results asserted: This includes both -// assertions in test code, and assertions in code covered by tests, -// like *Code Contract* assertions and *Debug.Assert(…)* assertions. -// -// But if you wish to run this code query, -// you'll need to consider test assemblies in your list of -// application assemblies analyzed by NDepend. -//]]> - Methods directly and indirectly called by test Methods - -let testAttr = from t in ThirdParty.Types.WithNameIn("FactAttribute", "TestAttribute", "TestCaseAttribute") select t -let testMethods = Methods.TaggedWithAnyAttributes(testAttr) - -// --- Uncomment this line if your test methods are in dedicated test assemblies --- -// let testAssemblies = testMethods.ParentAssemblies().ToHashSetEx() - -let depthOfCalledByTest = Application.Methods.DepthOfIsUsedByAny(testMethods) -from pair in depthOfCalledByTest -where pair.Value > 0 -orderby pair.Value ascending -// --- Uncomment this line if your test methods are in dedicated test assemblies --- -//&& !testAssemblies.Contains(pair.CodeElement.ParentAssembly) - -select new { - method = pair.CodeElement, - // (depthOfCalledByTests == 1) means that the method is directly called by tests - // (depthOfCalledByTests == 2) means that the method is directly called by a method directly called by tests - // … - depthOfCalledByTests = pair.Value, - nbLinesOfCode = pair.CodeElement.NbLinesOfCode, - // --- Uncomment this line if your project import some coverage data --- - // m.PercentageCoverage -} - -// -// This query lists all methods *directly or indirectly* called by tests methods. -// *Indirectly* called by a test means that a test method calls a method, that calls a method… -// From this recursion, a code metric named *depthOfCalledByTests* is inferred, -// The value *1* means directly called by test, -// the value *2* means called by a method that is called by a test… -// -// Overrides of virtual and abstract methods, called through polymorphism, are not listed. -// Methods solely invoked through a delegate are not listed. -// Methods solely invoked through reflection are not listed. -// -// We advise to not include test assemblies in code analyzed by NDepend. -// We estimate that it is acceptable and practical to lower the quality gate of test code, -// because the important measures for tests are: -// -// • The coverage ratio, -// -// • And the amount of logic results asserted: This includes both -// assertions in test code, and assertions in code covered by tests, -// like *Code Contract* assertions and *Debug.Assert(…)* assertions. -// -// But if you wish to run this code query, -// you'll need to consider test assemblies in your list of -// application assemblies analyzed by NDepend. -//]]> - - - Potentially Dead Types -// ND1700:PotentiallyDeadTypes - -warnif count > 0 -// Filter procedure for types that should'nt be considered as dead -let canTypeBeConsideredAsDeadProc = new Func( - t => !t.IsPublic && // Public types might be used by client applications of your assemblies. - t.Name != "Program" && - t.Name != "TestHub" && - !t.FullName.Contains("WebCam") && - !t.Name.EndsWith("Tests") && - !t.IsGeneratedByCompiler && - - // If you don't want to link NDepend.API.dll, you can use your own - // IsNotDeadCodeAttribute or UsedImplicitlyAttribute - // and adapt the source code of this rule. - !t.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - !t.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) && - - // Exclude static types that define only const fields - // because they cannot be seen as used in IL code. - !(t.IsStatic && t.NbMethods == 0 && !t.Fields.Where(f => !f.IsLiteral).Any()) && - - // Entity Framework ModelSnapshot classes are used ony by the EF infrastructure. - !t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch())) - -// Select types unused -let typesUnused = - from t in JustMyCode.Types where - t.NbTypesUsingMe == 0 && canTypeBeConsideredAsDeadProc(t) - select t - -// Dead types = types used only by unused types (recursive) -let deadTypesMetric = typesUnused.FillIterative( -types => from t in codeBase.Application.Types.UsedByAny(types).Except(types) - where canTypeBeConsideredAsDeadProc(t) && - t.TypesUsingMe.Intersect(types).Count() == t.NbTypesUsingMe - select t) - -from t in deadTypesMetric.DefinitionDomain -select new { - t, - depth = deadTypesMetric[t], - t.TypesUsingMe, - Debt = 15.ToMinutes().ToDebt(), - AnnualInterest = (10 + (t.NbLinesOfCode ?? 1)).ToMinutes().ToAnnualInterest() -} - -// -// This rule lists *potentially* **dead types**. -// A dead type is a type that can be removed -// because it is never used by the program. -// -// This rule lists not only types not used anywhere in code, -// but also types used only by types not used anywhere in code. -// This is why this rule comes with a column *TypesusingMe* and -// this is why there is a code metric named *depth*: -// -// • A *depth* value of *0* means the type is not used. -// -// • A *depth* value of *1* means the type is used only by types not used. -// -// • etc… -// -// By reading the source code of this rule, you'll see that by default, -// *public* types are not matched, because such type might not be used -// by the analyzed code, but still be used by client code, not analyzed by NDepend. -// This default behavior can be easily changed. -// -// Note that this rule doesn't match Entity Framework ModelSnapshot classes -// that are used ony by the EF infrastructure. -// - -// -// *Static analysis* cannot provide an *exact* list of dead types, -// because there are several ways to use a type *dynamically* (like through reflection). -// -// For each type matched by this query, first investigate if the type is used somehow -// (like through reflection). -// If the type is really never used, it is important to remove it -// to avoid maintaining useless code. -// If you estimate the code of the type might be used in the future, -// at least comment it, and provide an explanatory comment about the future intentions. -// -// If a type is used somehow, -// but still is matched by this rule, you can tag it with the attribute -// **IsNotDeadCodeAttribute** found in *NDepend.API.dll* to avoid matching the type again. -// You can also provide your own attribute for this need, -// but then you'll need to adapt this code rule. -// -// Issues of this rule have a **Debt** equal to 15 minutes because it only -// takes a short while to investigate if a type can be safely discarded. -// The **Annual Interest** of issues of this rule, the annual cost to not -// fix such issue, is proportional to the type #lines of code, because -// the bigger the type is, the more it slows down maintenance. -//]]> - Potentially Dead Methods -// ND1701:PotentiallyDeadMethods - -warnif count > 0 -// Filter procedure for methods that should'nt be considered as dead -let canMethodBeConsideredAsDeadProc = new Func( - m => !m.IsPubliclyVisible && // Public methods might be used by client applications of your assemblies. - !m.IsEntryPoint && // Main() method is not used by-design. - !m.IsExplicitInterfaceImpl && // The IL code never explicitly calls explicit interface methods implementation. - !m.IsClassConstructor && // The IL code never explicitly calls class constructors. - !m.IsFinalizer && // The IL code never explicitly calls finalizers. - !m.IsVirtual && // Only check for non virtual method that are not seen as used in IL. - !(m.IsConstructor && // Don't take account of protected ctor that might be call by a derived ctors. - m.IsProtected) && - !m.IsEventAdder && // The IL code never explicitly calls events adder/remover. - !m.IsEventRemover && - !m.IsGeneratedByCompiler && - !m.ParentType.IsDelegate && - !m.IsInternal && //take out internally used stuff, mostly called by reflection - !m.ParentType.Name.Contains("Tests") && - !m.ParentType.FullName.Contains("Mocks") && - !m.Name.StartsWith(".ctor(") && - // Properties of ContentPattern are filled via Reflection and not by the (private) setter - m.ParentType.BaseClass != null && - m.ParentType.BaseClass.FullName != "System.MarshalByRefObject" && - m.Name != "GetRendererIndex" && - m.Name != "CopyDllsIfNeeded" && - m.Name != "AreNativeDllsMissingOrWrongCpuFormat" && - m.Name != "IsNativeDllInWrongFormat" && - m.Name != "GetPathOfNativeBinariesDirectory" && - m.Name != "TryCopyNativeDlls" && - m.Name != "TryCopyNativeDllsFromNuGetPackage" && - m.Name != "CopyNativeDllsFromPath" && - m.Name != "AreNativeDllsCopiedFromLocalNativeBinariesFolder" && - m.Name != "get_Is3D" && - m.Name != "PrintMembers(StringBuilder)" && - !m.ParentType.Name.Contains("Image") && - !m.ParentType.Name.Contains("Font") && - !m.ParentType.Name.Contains("Geometry") && - !m.ParentType.Name.Contains("Shader") && - !m.FullName.Contains("WebCam") && - !m.FullName.Contains("NVorbis") && - m.ParentType.Name != "SpineData" && - - // Don't consider Global ASP.NET methods as unused - !m.ParentType.DeriveFrom("System.Web.HttpApplication".AllowNoMatch()) && - - // Methods tagged with these attributes are called by the serialization infrastructure. - !m.HasAttribute("System.Runtime.Serialization.OnSerializingAttribute".AllowNoMatch()) && - !m.HasAttribute("System.Runtime.Serialization.OnDeserializingAttribute".AllowNoMatch()) && - !m.HasAttribute("System.Runtime.Serialization.OnSerializedAttribute".AllowNoMatch()) && - !m.HasAttribute("System.Runtime.Serialization.OnDeserializedAttribute".AllowNoMatch()) && - - // If you don't want to link NDepend.API.dll, you can use your own - // IsNotDeadCodeAttribute or UsedImplicitlyAttribute - // and adapt the source code of this rule. - !m.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - !m.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) && - - // Don't consider public getters/setters - // of classes that implement INotifyPropertyChanged - // as dead code. - !(m.IsPublic && (m.IsPropertyGetter || m.IsPropertySetter) && - m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch())) - ) - -// Get methods unused -let methodsUnused = - from m in JustMyCode.Methods where - m.NbMethodsCallingMe == 0 && - canMethodBeConsideredAsDeadProc(m) - select m - -// Dead methods = methods used only by unused methods (recursive) -let deadMethodsMetric = methodsUnused.FillIterative( - methods => // Unique loop, just to let a chance to build the hashset. - from o in (new object()).ToEnumerable() - // Use a hashet to make Intersect calls much faster! - let hashset = methods.ToHashSetEx() - from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods) - where canMethodBeConsideredAsDeadProc(m) && - // Select methods called only by methods already considered as dead - hashset.Intersect(m.MethodsCallingMe).Count() == m.NbMethodsCallingMe - select m) - -from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain) -let depth = deadMethodsMetric[m] -select new { - m, - depth, - m.MethodsCallingMe, - Debt = (10 + 3*depth).ToMinutes().ToDebt(), - AnnualInterest = (8 + (m.NbLinesOfCode ?? 1)).ToMinutes().ToAnnualInterest() -} - -// -// This rule lists *potentially* **dead methods**. -// A dead method is a method that can be removed -// because it is never called by the program. -// -// This rule lists not only methods not called anywhere in code, -// but also methods called only by methods not called anywhere in code. -// This is why this rule comes with a column *MethodsCallingMe* and -// this is why there is a code metric named *depth*: -// -// • A *depth* value of *0* means the method is not called. -// -// • A *depth* value of *1* means the method is called only by methods not called. -// -// • etc… -// -// By reading the source code of this rule, you'll see that by default, -// *public* methods are not matched, because such method might not be called -// by the analyzed code, but still be called by client code, not analyzed by NDepend. -// This default behavior can be easily changed. -// - -// -// *Static analysis* cannot provide an *exact* list of dead methods, -// because there are several ways to invoke a method *dynamically* (like through reflection). -// -// For each method matched by this query, first investigate if the method is invoked somehow -// (like through reflection). -// If the method is really never invoked, it is important to remove it -// to avoid maintaining useless code. -// If you estimate the code of the method might be used in the future, -// at least comment it, and provide an explanatory comment about the future intentions. -// -// If a method is invoked somehow, -// but still is matched by this rule, you can tag it with the attribute -// **IsNotDeadCodeAttribute** found in *NDepend.API.dll* to avoid matching the method again. -// You can also provide your own attribute for this need, -// but then you'll need to adapt this code rule. -// -// Issues of this rule have a **Debt** equal to 10 minutes because it only -// takes a short while to investigate if a method can be safely discarded. -// On top of these 10 minutes, the depth of usage of such method adds up -// 3 minutes per unity because dead method only called by dead code -// takes a bit more time to be investigated. -// -// The **Annual Interest** of issues of this rule, the annual cost to not -// fix such issue, is proportional to the type #lines of code, because -// the bigger the method is, the more it slows down maintenance. -//]]> - Potentially Dead Fields -// ND1702:PotentiallyDeadFields - -warnif count > 0 -from f in JustMyCode.Fields where - f.NbMethodsUsingMe == 0 && - !f.IsPublic && // Although not recommended, public fields might be used by client applications of your assemblies. - !f.IsLiteral && // The IL code never explicitly uses literal fields. - !f.IsEnumValue && // The IL code never explicitly uses enumeration value. - f.Name != "value__" && // Field named 'value__' are relative to enumerations and the IL code never explicitly uses them. - !f.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - !f.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) && - !f.IsGeneratedByCompiler && - !f.FullName.Contains("WglGraphicsContext") && - !f.FullName.Contains("WindowsDisplayDevice") && - !f.FullName.Contains("WindowsGamePad") && - !f.FullName.Contains("NativeTouchInput") - // If you don't want to link NDepend.API.dll, you can use your own IsNotDeadCodeAttribute - // and adapt the source code of this rule. -select new { - f, - Debt = 10.ToMinutes().ToDebt(), - AnnualInterest = 8.ToMinutes().ToAnnualInterest() -} - -// -// This rule lists *potentially* **dead fields**. -// A dead field is a field that can be removed -// because it is never used by the program. -// -// By reading the source code of this rule, you'll see that by default, -// *public* fields are not matched, because such field might not be used -// by the analyzed code, but still be used by client code, not analyzed by NDepend. -// This default behavior can be easily changed. -// Some others default rules in the *Visibility* group, warn about public fields. -// -// More restrictions are applied by this rule because of some *by-design* limitations. -// NDepend mostly analyzes compiled IL code, and the information that -// an enumeration value or a literal constant (which are fields) is used -// is lost in IL code. Hence by default this rule won't match such field. -// - -// -// *Static analysis* cannot provide an *exact* list of dead fields, -// because there are several ways to assign or read a field *dynamically* -// (like through reflection). -// -// For each field matched by this query, first investigate -// if the field is used somehow (like through reflection). -// If the field is really never used, it is important to remove it -// to avoid maintaining a useless code element. -// -// If a field is used somehow, -// but still is matched by this rule, you can tag it with the attribute -// **IsNotDeadCodeAttribute** found in *NDepend.API.dll* -// to avoid matching the field again. -// You can also provide your own attribute for this need, -// but then you'll need to adapt this code rule. -// -// Issues of this rule have a **Debt** equal to 10 minutes because it only -// takes a short while to investigate if a method can be safely discarded. -// The **Annual Interest** of issues of this rule, the annual cost to not -// fix such issue, is set by default to 8 minutes per unused field matched. -//]]> - Wrong usage of IsNotDeadCodeAttribute -// ND1703:WrongUsageOfIsNotDeadCodeAttribute - -warnif count > 0 - -let tAttr = Types.WithFullName("NDepend.Attributes.IsNotDeadCodeAttribute").FirstOrDefault() -where tAttr != null - -// Get types that do a wrong usage of IsNotDeadCodeAttribute -let types = from t in Application.Types where - t.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - - ( // types used don't need to be tagged with IsNotDeadCodeAttribute! - t.TypesUsingMe.Count(t1 => - !t.NestedTypes.Contains(t1) && - !t1.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) ) > 0 || - - // Static types that define only const fields cannot be seen as used in IL code. - // They don't need to be tagged with IsNotDeadCodeAttribute. - (t.IsStatic && t.NbMethods == 0 && !t.Fields.Where(f => !f.IsLiteral).Any()) - ) - select t - -// Get methods that do a wrong usage of IsNotDeadCodeAttribute -let methods = from m in Application.Methods where - m.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - m.MethodsCallingMe.Count(m1 => !m1.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch())) > 0 - select m - -// Get fields that do a wrong usage of IsNotDeadCodeAttribute -let fields = from f in Application.Fields where - f.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - f.MethodsUsingMe.Count(m1 => !m1.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch())) > 0 - select f - -from member in types.Cast().Concat(methods).Concat(fields) -select new { - member, - Debt = 4.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// The attribute **NDepend.Attributes.IsNotDeadCodeAttribute** -// is defined in *NDepend.API.dll*. This attribute is used -// to mean that a code element is not used directly, but is used -// somehow, like through reflection. -// -// This attribute is used in the dead code rules, -// *Potentially dead Types*, *Potentially dead Methods* -// and *Potentially dead Fields*. -// If you don't want to link *NDepend.API.dll*, you can use -// your own *IsNotDeadCodeAttribute* and adapt the source code of -// this rule, and the source code of the *dead code* rules. -// -// In this context, this code rule matches code elements -// (types, methods, fields) that are tagged with this attribute, -// but still used directly somewhere in the code. -// - -// -// Just remove *IsNotDeadCodeAttribute* tagging of -// types, methods and fields matched by this rule -// because this tag is not useful anymore. -//]]> - - - Don't use CoSetProxyBlanket and CoInitializeSecurity -// ND3100:DontUseCoSetProxyBlanketAndCoInitializeSecurity -warnif count > 0 - -from m in Application.Methods -where (m.HasAttribute ("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch())) - && m.SimpleName.EqualsAny("CoSetProxyBlanket","CoInitializeSecurity") - -select new { - m, - Debt = 1.ToHours().ToDebt(), - AnnualInterest = 2.ToHours().ToAnnualInterest() -} - -// -// As soon as some managed code starts being JIT’ed and executed by the CLR, -// it is too late to call *CoInitializeSecurity()* or *CoSetProxyBlanket()*. -// By this point in time, the CLR has already initialized the -// COM security environment for the entire Windows process. -// - -// -// Don't call CoSetProxyBlanket() or CoInitializeSecurity() from managed code. -// -// Instead write an unmanaged "shim" in C++ that will call -// one or both methods before loading the CLR within the process. -// -// More information about writing such unmanaged "shim" -// can be found in this StackOverflow answer: -// https://stackoverflow.com/a/48545055/27194 -//]]> - Don't use System.Random for security purposes -// ND3101:DontUseSystemRandomForSecurityPurposes -warnif count > 0 - -from m in Application.Methods -where m.CreateA("System.Random".AllowNoMatch()) -select new { - m, - Debt = 15.ToMinutes().ToDebt(), - AnnualInterest = 1.ToHours().ToAnnualInterest() -} - -// -// The algorithm used by the implementation of **System.Random** is weak -// because random numbers generated can be predicted. -// -// Using predictable random values in a security critical context -// can lead to vulnerabilities. -// - -// -// If the matched method is meant to be executed in a security -// critical context use **System.Security.Cryptography.RandomNumberGenerator** -// or **System.Security.Cryptography.RNGCryptoServiceProvider** instead. -// These random implementations are slower to execute but the random numbers -// generated cannot be predicted. -// -// Find more on using *RNGCryptoServiceProvider* to generate random values here: -// https://stackoverflow.com/questions/32932679/using-rngcryptoserviceprovider-to-generate-random-string -// -// Otherwise you can use the faster **System.Random** implementation and -// suppress corresponding issues. -// -// More information about the weakness of *System.Random* implementation -// can be found here: https://stackoverflow.com/a/6842191/27194 -//]]> - Don't use DES/3DES weak cipher algorithms -// ND3102:DontUseDES3DESWeakCipherAlgorithms -warnif count > 0 - -from m in Application.Methods -where - m.CreateA("System.Security.Cryptography.TripleDESCryptoServiceProvider".AllowNoMatch()) || - m.CreateA("System.Security.Cryptography.DESCryptoServiceProvider".AllowNoMatch()) || - m.IsUsing("System.Security.Cryptography.DES.Create()".AllowNoMatch()) || - m.IsUsing("System.Security.Cryptography.DES.Create(String)".AllowNoMatch()) -select new { - m, - Debt = 1.ToHours().ToDebt(), - Severity = Severity.High -} - -// -// Since 2005 the NIST, the US National Institute of Standards and Technology, -// doesn't consider DES and 3DES cypher algorithms as secure. Source: -// https://www.nist.gov/news-events/news/2005/06/nist-withdraws-outdated-data-encryption-standard -// - -// -// Use the AES (Advanced Encryption Standard) algorithms instead -// with the .NET Framework implementation: -// *System.Security.Cryptography.AesCryptoServiceProvider*. -// -// You can still suppress issues of this rule when using -// DES/3DES algorithms for compatibility reasons with legacy applications and data. -//]]> - Don't disable certificate validation -// ND3103:DontDisableCertificateValidation -warnif count > 0 - -from m in Application.Methods -where - m.IsUsing("System.Net.ServicePointManager.set_ServerCertificateValidationCallback(RemoteCertificateValidationCallback)".AllowNoMatch()) -select new { - m, - Debt = 30.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// Matched methods are subscribing a custom certificate validation -// procedure through the delegate: *ServicePointManager.ServerCertificateValidationCallback*. -// -// Doing so is often used to disable certificate validation -// to connect easily to a host that is not signed by a **root certificate authority**. -// https://en.wikipedia.org/wiki/Root_certificate -// -// This creates a **vulnerability to man-in-the-middle attacks** since the client will trust any certificate. -// https://en.wikipedia.org/wiki/Man-in-the-middle_attack -// - -// -// Don't rely on a weak custom certificate validation. -// -// If a legitimate custom certificate validation procedure must be subscribed, -// you can chose to suppress related issue(s). -//]]> - Review publicly visible event handlers -// ND3104:ReviewPubliclyVisibleEventHandlers -warnif count > 0 -from m in Application.Methods -where - m.IsPubliclyVisible && - m.SimpleName == "FormsWindow" && - m.Name.EndsWith("(Object,EventArgs)") -select new { - m, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Low -} - - -// -// Review publicly visible event handlers to check those that are -// running security critical actions. -// -// An event handler is any method with the standard signature *(Object,EventArgs)*. -// An event handler can be registered to any event matching this standard signature. -// -// As a consequence, such event handler can be subscribed -// by malicious code to an event to provoke execution of the -// security critical action on event firing. -// -// Even if such event handler does a security check, -// it can be executed from a chain of trusted callers on the call stack, -// and cannot detect about malicious registration. -// - -// -// Change matched event handlers to make them non-public. -// Preferably don't run a security critical action from an event handler. -// -// If after a careful check no security critical action is involved -// from a matched event-handler, you can suppress the issue. -//]]> - Pointers should not be publicly visible -// ND3105:PointersShouldNotBePubliclyVisible - -warnif count > 0 -from f in Application.Fields -where - f.FieldType != null && - f.FieldType.FullName.EqualsAny("System.IntPtr","System.UIntPtr") && - (f.IsPubliclyVisible || f.IsProtected) -let methodsUserOutsideMyAssembly = f.MethodsUsingMe.Where(m => m.ParentAssembly != m.ParentAssembly) -select new { - f, - f.FieldType, - methodsUserOutsideMyAssembly, - Debt = (15 + 10*methodsUserOutsideMyAssembly.Count()).ToMinutes().ToDebt(), - Severity = f.IsInitOnly ? Severity.Medium : Severity.High -} - -// -// Pointers should not be exposed publicly. -// -// This rule detects fields with type *System.IntPtr* or *System.UIntPtr* -// that are public or protected. -// -// Pointers are used to access unmanaged memory from managed code. -// Exposing a pointer publicly makes it easy for malicious code -// to read and write unmanaged data used by the application. -// -// The situation is even worse if the field is not read-only -// since malicious code can change it and force the application -// to rely on arbitrary data. -// - -// -// Pointers should have the visibility - private or internal. -// -// The estimated Debt, which means the effort to fix such issue, -// is 15 minutes and 10 additional minutes per method using the field outside its assembly. -// -// The estimated Severity of such issue is *Medium*, and *High* -// if the field is non read-only. -//]]> - Seal methods that satisfy non-public interfaces -// ND3106:SealMethodsThatSatisfyNonPublicInterfaces - -warnif count > 0 -from m in Application.Methods -where - !m.IsFinal && // If the method is not marked as virtual the flag final is set - m.IsPubliclyVisible && - !m.ParentType.IsSealed && - m.ParentType.IsClass -let overridenInterface = - m.OverriddensBase.FirstOrDefault(mb => mb.ParentType.IsInterface && !mb.ParentType.IsPubliclyVisible) -where overridenInterface != null -select new { - m, - overridenInterface = overridenInterface.ParentType, - Debt = 30.ToMinutes().ToDebt(), - Severity = Severity.High - } - -// -// A match of this rule represents a virtual method, publicly visible, -// defined in a non-sealed public class, that overrides a method of an -// internal interface. -// -// The interface not being public indicates a process that -// should remain private. -// -// Hence this situation represents a security vulnerability because -// it is now possible to create a malicious class, that derives from -// the parent class and that overrides the method behavior. -// This malicious behavior will be then invoked by private implementation. -// - -// -// You can: -// -// - seal the parent class, -// -// - or change the accessibility of the parent class to non-public, -// -// - or implement the method without using the *virtual* modifier, -// -// - or change the accessibility of the method to non-public. -// -// If after a careful check such situation doesn't represent -// a security threat, you can suppress the issue. -//]]> - Review commands vulnerable to SQL injection -// ND3107:ReviewCommandsVulnerableToSQLInjection - -warnif count > 0 - -let commands = ThirdParty.Types.WithFullNameIn( - "System.Data.Common.DbCommand", - "System.Data.SqlClient.SqlCommand", - "System.Data.OleDb.OleDbCommand", - "System.Data.Odbc.OdbcCommand", - "System.Data.OracleClient.OracleCommand") -where commands.Any() -let commandCtors = commands.ChildMethods().Where(m => m.IsConstructor).ToHashSetEx() -let commandExecutes = commands.ChildMethods().Where(m => m.SimpleName.Contains("Execute")).ToHashSetEx() -let commandParameters = commands.ChildMethods().Where(m => m.IsPropertyGetter && m.SimpleName == "get_Parameters").ToHashSetEx() - - -from m in Application.Methods.UsingAny(commandCtors) - .UsingAny(commandExecutes) -where !m.MethodsCalled.Intersect(commandParameters).Any() && - // Check also that non of the method call rely on command parameters to get less false positives: - !m.MethodsCalled.SelectMany(mc => mc.MethodsCalled).Intersect(commandParameters).Any() - -select new { m, - createA = m.MethodsCalled.Intersect(commandCtors).First().ParentType, - calls = m.MethodsCalled.Intersect(commandExecutes).First(), - Debt = 15.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule matches methods that create a DB command -// (like an *SqlCommand* or an *OleDbCommand*) -// that call an *Execute* command method (like *ExecuteScalar()*) -// and that don't use command parameters. -// -// This situation is prone to **SQL Injection** https://en.wikipedia.org/wiki/SQL_injection -// since malicious SQL code might be injected in string parameters values. -// -// However there might be false positives. -// So review carefully matched methods -// and use suppress issues when needed. -// -// To limit the false positives, this rule also checks whether -// command parameters are accessed from any sub method call. -// This is a solid indication of non-vulnerability. -// - -// -// If after a careful check it appears that the method is indeed -// using some strings to inject parameters values in the SQL query string, -// **command.Parameters.Add(...)** must be used instead. -// -// You can get more information on adding parameters explicitely here: -// https://stackoverflow.com/questions/4892166/how-does-sqlparameter-prevent-sql-injection -// ]]> - Review data adapters vulnerable to SQL injection -// ND3108:ReviewDataAdaptersVulnerableToSQLInjection - -warnif count > 0 - -let adapters = ThirdParty.Types.WithFullNameIn( - "System.Data.Common.DataAdapter", - "System.Data.Common.DbDataAdapter", - "System.Data.SqlClient.SqlDataAdapter", - "System.Data.OleDb.OleDbDataAdapter", - "System.Data.Odbc.OdbcDataAdapter", - "System.Data.OracleClient.OracleDataAdapter") - -where adapters.Any() -let adapterCtors = adapters.ChildMethods().Where(m => m.IsConstructor).ToHashSetEx() -let adapterFill = adapters.ChildMethods().Where(m => m.SimpleName.Contains("Fill")).ToHashSetEx() - - -let commands = ThirdParty.Types.WithFullNameIn( - "System.Data.Common.DbCommand", - "System.Data.SqlClient.SqlCommand", - "System.Data.OleDb.OleDbCommand", - "System.Data.Odbc.OdbcCommand", - "System.Data.OracleClient.OracleCommand") -where commands.Any() -let commandParameters = commands.ChildMethods().Where(m => m.IsPropertyGetter && m.SimpleName == "get_Parameters").ToHashSetEx() - - -from m in Application.Methods.UsingAny(adapterCtors) - .UsingAny(adapterFill) -where !m.MethodsCalled.Intersect(commandParameters).Any() && - // Check also that non of the method call rely on command parameters to get less false positives: - !m.MethodsCalled.SelectMany(mc => mc.MethodsCalled).Intersect(commandParameters).Any() - -select new { m, - createA = m.MethodsCalled.Intersect(adapterCtors).First().ParentType, - calls = m.MethodsCalled.Intersect(adapterFill).First(), - Debt = 15.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule matches methods that create a DB adapter -// (like an *SqlDataAdapter* or an *OleDbDataAdapter*) -// that call the method *Fill()* -// and that don't use DB command parameters. -// -// This situation is prone to **SQL Injection** https://en.wikipedia.org/wiki/SQL_injection -// since malicious SQL code might be injected in string parameters values. -// -// However there might be false positives. -// So review carefully matched methods -// and use suppress issues when needed. -// -// To limit the false positives, this rule also checks whether -// command parameters are accessed from any sub method call. -// This is a solid indication of non-vulnerability. -// - -// -// If after a careful check it appears that the method is indeed -// using some strings to inject parameters values in the SQL query string, -// **adapter.SelectCommand.Parameters.Add(...)** must be used instead -// (or *adapter.UpdateCommand* or *adapter.InsertCommand*, depending on the context). -// -// You can get more information on adding parameters explicitely here: -// https://stackoverflow.com/questions/4892166/how-does-sqlparameter-prevent-sql-injection -// ]]> - - - Methods that could have a lower visibility -// ND1800:MethodsThatCouldHaveALowerVisibility - -warnif count > 0 from m in JustMyCode.Methods where - m.Visibility != m.OptimalVisibility && - - !m.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) && - !m.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - !m.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) && - // If you don't want to link NDepend.API.dll, you can use your own attributes - // and adapt the source code of this rule. - - // methods of serialized type must remain public. - !m.ParentType.TypesUsed.Any(t1 => t1.IsAttributeClass && t1.ParentNamespace.Name == "Newtonsoft.Json") && - !m.ParentType.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) && - !m.ParentType.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) && - - // Eliminate public methods visible outside of their assembly - // because the rule cannot know if the developer left the method public - // intentionally or not. - !m.IsPubliclyVisible && - !m.FullName.Contains("Xml") && - !m.IsConstructor && - !m.Name.StartsWith("GetSolutionParentFolder(String)") && - !m.Name.StartsWith("ComponentValidator") && - // Avoid matching public methods declared in a non-public type, - // that could have the visibility internal, because - // such situation is caught by the rule 'Avoid public methods not publicly visible'. - !(m.Visibility == Visibility.Public && - m.ParentType.Visibility != Visibility.Public && - m.OptimalVisibility == Visibility.Internal) && - - // Eliminate default constructor from the result. - // Whatever the visibility of the declaring class, - // default constructors are public and introduce noise - // in the current rule. - !( m.IsConstructor && m.IsPublic && m.NbParameters == 0) && - - // Don't advise to reduce visibility of property getters/setters - // of classes that implement INotifyPropertyChanged - !((m.IsPropertyGetter || m.IsPropertySetter) && - m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch())) && - - // Don't decrease the visibility of Main() methods. - !m.IsEntryPoint - -select new { - m, - m.Visibility , - CouldBeDeclared = m.OptimalVisibility, - m.MethodsCallingMe, - - Debt = 30.ToSeconds().ToDebt(), // It is fast to change the method visibility - Severity = Severity.Medium -} - -// -// This rule warns about methods that can be declared with a lower visibility -// without provoking any compilation error. -// For example *private* is a visibility lower than *internal* -// which is lower than *public*. -// -// **Narrowing visibility** is a good practice because doing so **promotes encapsulation**. -// The scope from which methods can be called is then reduced to a minimum. -// -// By default, this rule doesn't match publicly visible methods that could have a -// lower visibility because it cannot know if the developer left the method public -// intentionally or not. Public methods matched are declared in non-public types. -// -// By default this rule doesn't match methods with the visibility *public* -// that could be *internal*, declared in a type that is not *public* -// (internal, or nested private for example) because -// this situation is caught by the rule *Avoid public methods not publicly visible*. -// -// Notice that methods tagged with one of the attribute -// *NDepend.Attributes.CannotDecreaseVisibilityAttribute* or -// *NDepend.Attributes.IsNotDeadCodeAttribute*, found in *NDepend.API.dll* -// are not matched. If you don't want to link *NDepend.API.dll* but still -// wish to rely on this facility, you can declare these attributes in your code. -// - -// -// Declare each matched method with the specified *optimal visibility* -// in the *CouldBeDeclared* rule result column. -// -// By default, this rule matches *public methods*. If you are publishing an API -// many public methods matched should remain public. In such situation, -// you can opt for the *coarse solution* to this problem by adding in the -// rule source code *&& !m.IsPubliclyVisible* or you can prefer the -// *finer solution* by tagging each concerned method with -// *CannotDecreaseVisibilityAttribute*. -//]]> - Types that could have a lower visibility -// ND1801:TypesThatCouldHaveALowerVisibility - -warnif count > 0 from t in JustMyCode.Types where - - t.Visibility != t.OptimalVisibility && - - // If you don't want to link NDepend.API.dll, you can use your own attributes - // and adapt the source code of this rule. - !t.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) && - !t.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - !t.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) && - - // JSON and XML serialized type must remain public. - !t.IsUsing("Newtonsoft.Json".MatchNamespace().AllowNoMatch()) && - !t.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) && - !t.FullName.Contains("ComponentValidator") && - !t.FullName.Contains("EntityTemplate") && - !t.FullName.Contains("BinaryData") && - !t.FullName.Contains("SceneRepository") && - !t.FullName.Contains("WglGraphicsContext") && - !t.FullName.Contains("EntityUpdater") && - !t.FullName.Contains("AppRunner") && - !t.FullName.Contains("SceneEntityComponentFiller") && - !t.FullName.Contains("SceneContentEntityRepository") && - !t.FullName.Contains("PathExtensions") && - !t.FullName.Contains("SplitSameComponents") && - - // Eliminate public types visible outside of their assembly - // because the rule cannot know if the developer left the type public - // intentionally or not. - !t.IsPubliclyVisible && - - // Static types that define only const fields cannot be seen as used in IL code. - // They don't have to be tagged with CannotDecreaseVisibilityAttribute. - !( t.IsStatic && - !t.Methods.Any(m => !m.IsClassConstructor) && - !t.Fields.Any(f => !f.IsLiteral && !(f.IsStatic && f.IsInitOnly))) && - - // A type used by an interface that has the same visibility - // cannot have its visibility decreased, else a compilation error occurs! - !t.TypesUsingMe.Any(tUser => - tUser.IsInterface && - tUser.Visibility == t.Visibility) && - - // Don't change the visibility of a type that contain an entry point method. - !t.Methods.Any(m =>m.IsEntryPoint) - -select new { - t, - t.Visibility , - CouldBeDeclared = t.OptimalVisibility, - t.TypesUsingMe, - - Debt = 30.ToSeconds().ToDebt(), // It is fast to change the method visibility - Severity = Severity.Medium -} - -// -// This rule warns about types that can be declared with a lower visibility -// without provoking any compilation error. -// For example *private* is a visibility lower than *internal* -// which is lower than *public*. -// -// **Narrowing visibility** is a good practice because doing so **promotes encapsulation**. -// The scope from which types can be consumed is then reduced to a minimum. -// -// By default, this rule doesn't match publicly visible types that could have -// a lower visibility because it cannot know if the developer left the type public -// intentionally or not. Public types matched are nested in non-public types. -// -// Notice that types tagged with one of the attribute -// *NDepend.Attributes.CannotDecreaseVisibilityAttribute* or -// *NDepend.Attributes.IsNotDeadCodeAttribute*, found in *NDepend.API.dll* -// are not matched. If you don't want to link *NDepend.API.dll* but still -// wish to rely on this facility, you can declare these attributes in your code. -// - -// -// Declare each matched type with the specified *optimal visibility* -// in the *CouldBeDeclared* rule result column. -// -// By default, this rule matches *public types*. If you are publishing an API -// many public types matched should remain public. In such situation, -// you can opt for the *coarse solution* to this problem by adding in the -// rule source code *&& !m.IsPubliclyVisible* or you can prefer the -// *finer solution* by tagging each concerned type with -// *CannotDecreaseVisibilityAttribute*. -//]]> - Fields that could have a lower visibility -// ND1802:FieldsThatCouldHaveALowerVisibility - -warnif count > 0 from f in JustMyCode.Fields where - f.Visibility != f.OptimalVisibility && - !f.FullName.Contains("Scene") && - !f.ParentType.Name.EndsWith("Renderer") && - !f.ParentType.Name.EndsWith("Processor") && - !f.ParentType.Name.EndsWith("Initializer") && - !f.ParentType.Name.EndsWith("Updater") && - !f.ParentType.Name.EndsWith("MainWindow") && - f.OptimalVisibility != NDepend.CodeModel.Visibility.Internal && - !f.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) && - !f.HasAttribute("NDepend.Attributes.IsNotDeadCodeAttribute".AllowNoMatch()) && - !f.HasAttribute("JetBrains.Annotations.UsedImplicitlyAttribute".AllowNoMatch()) && - // If you don't want to link NDepend.API.dll, you can use your own attributes - // and adapt the source code of this rule. - - // JSON and XML serialized fields must remain public. - !f.ParentType.TypesUsed.Any(t1 => t1.IsAttributeClass && t1.ParentNamespace.Name == "Newtonsoft.Json") && - !f.HasAttribute("System.Xml.Serialization.XmlElementAttribute".AllowNoMatch()) && - !f.HasAttribute("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) && - !f.HasAttribute("System.Xml.Serialization.XmlArrayAttribute".AllowNoMatch()) && - !f.HasAttribute("System.Xml.Serialization.XmlArrayItemAttribute".AllowNoMatch()) && - - // Don't check for serialized fields visibility - !f.HasAttribute("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) && - - // Eliminate public fields visible outside of their assembly - // because the rule cannot know if the developer left the field public - // intentionally or not. - !f.IsPubliclyVisible - -select new { - f, - f.Visibility , - CouldBeDeclared = f.OptimalVisibility, - f.MethodsUsingMe, - - Debt = 30.ToSeconds().ToDebt(), // It is fast to change the field visibility - Severity = Severity.Medium -} - -// -// This rule warns about fields that can be declared with a lower visibility -// without provoking any compilation error. -// For example *private* is a visibility lower than *internal* -// which is lower than *public*. -// -// **Narrowing visibility** is a good practice because doing so **promotes encapsulation**. -// The scope from which fields can be consumed is then reduced to a minimum. -// -// By default, this rule doesn't match publicly visible fields that could have a -// lower visibility because it cannot know if the developer left the field public -// intentionally or not. Public fields matched are declared in non-public types. -// -// Notice that fields tagged with one of the attribute -// *NDepend.Attributes.CannotDecreaseVisibilityAttribute* or -// *NDepend.Attributes.IsNotDeadCodeAttribute*, found in *NDepend.API.dll* -// are not matched. If you don't want to link *NDepend.API.dll* but still -// wish to rely on this facility, you can declare these attributes in your code. -// - -// -// Declare each matched field with the specified *optimal visibility* -// in the *CouldBeDeclared* rule result column. -// -// By default, this rule matches *public fields*. If you are publishing an API -// some public fields matched should remain public. In such situation, -// you can opt for the *coarse solution* to this problem by adding in the -// rule source code *&& !m.IsPubliclyVisible* or you can prefer the -// *finer solution* by tagging eah concerned field with -// *CannotDecreaseVisibilityAttribute*. -//]]> - Types that could be declared as private, nested in a parent type -// ND1803:TypesThatCouldBeDeclaredAsPrivateNestedInAParentType - -warnif count > 0 -from t in JustMyCode.Types -where !t.IsGeneratedByCompiler && - !t.IsNested && - !t.IsPubliclyVisible && - !t.IsEnumeration && - // Only one type user - t.TypesUsingMe.Count() == 1 && - t.Name != "NativeMethods" && - t.Name != "DotBulk" && - t.Name != "Create3DPointCloud" && - t.Name != "SceneEntityComponentFiller" && - t.Name != "SharedComponentNodes" && - t.Name != "ComponentValidityChecker" && - t.Name != "AssemblyTypeLoader" && - t.Name != "EngineContentResolver" && - t.Name != "BinaryDataSaver" && - t.Name != "NetworkExtensions" && - t.Name != "MergedPixel" - - -let couldBeNestedIn = t.TypesUsingMe.Single() -where !couldBeNestedIn.IsGeneratedByCompiler && - !couldBeNestedIn.IsInterface && // Cannot nest a type in an interface - // …declared in the same namespace - couldBeNestedIn.ParentNamespace == t.ParentNamespace && - - // Don't advise to move a base class - // or an interface into one of its child type. - !couldBeNestedIn.DeriveFrom(t) && - !couldBeNestedIn.Implement(t) - - // Require that t doesn't contain any extension method. - // Types with extension methods cannot be nested. -where t.Methods.All(m => !m.IsExtensionMethod) - -select new { - t, - couldBeNestedIn, - Debt = 3.ToMinutes().ToDebt(), // It is fast to nest a type into another one - Severity = Severity.Low // This rule proposes advices, not potential problems -} - -// -// This rule matches types that can be potentially -// *nested* and declared *private* into another type. -// -// The conditions for a type to be potentially nested -// into a *parent type* are: -// -// • the *parent type* is the only type consuming it, -// -// • the type and the *parent type* are declared in the same namespace. -// -// Declaring a type as private into a parent type **promotes encapsulation**. -// The scope from which the type can be consumed is then reduced to a minimum. -// -// This rule doesn't match classes with extension methods -// because such class cannot be nested in another type. -// - -// -// Nest each matched *type* into the specified *parent type* and -// declare it as private. -// -// However *nested private types* are hardly testable. Hence this rule -// might not be applied to types consumed directly by tests. -//]]> - Avoid publicly visible constant fields -// ND1804:AvoidPubliclyVisibleConstantFields - -warnif count > 0 -from f in JustMyCode.Fields -where f.IsLiteral && - f.IsPubliclyVisible && - !f.IsEnumValue && - !f.ParentType.IsGeneric && -f.Name != "DefaultUpdatesPerSecond" && -f.Name != "EditorFixedToken" && -f.Name != "Phi" && -f.Name != "MaximumNestingDepth" && -f.Name != "DefaultMaxInputUpdatesPerSecond" && -f.Name != "VisualApprovalTestsFolder" && -f.Name != "Pi" && -f.Name != "FullCircleDegrees" && -f.Name != "HalfCircleDegrees" && -f.Name != "QuarterCircleDegrees" && -f.Name != "Epsilon" && -!f.Name.StartsWith("Default") && -!f.Name.EndsWith("Tick") && -!f.Name.EndsWith("Ticks") && -!f.FullName.Contains("Scene") && -!f.ParentNamespace.Name.Contains("Entities") -select new { - f, - - Debt = 30.ToSeconds().ToDebt(), // It is fast to update field declaration - Severity = Severity.Medium -} - -// -// This rule warns about constant fields that are visible outside their -// parent assembly. Such field, when used from outside its parent assembly, -// has its constant value *hard-coded* into the client assembly. -// Hence, when changing the field's value, it is *mandatory* to recompile -// all assemblies that consume the field, else the program will run -// with different constant values in-memory. Certainly in such situation -// bugs are lurking. -// - -// -// Declare matched fields as **static readonly** instead of **constant**. -// This way, the field value is *safely changeable* without the need to -// recompile client assemblies. -// -// Notice that enumeration value fields suffer from the same *potential -// pitfall*. But enumeration values cannot be declared as -// *static readonly* hence the rule comes with the condition -// **&& !f.IsEnumValue** to avoid matching these. Unless you decide -// to banish public enumerations, just let the rule *as is*. -//]]> - Fields should be declared as private -// ND1805:FieldsShouldBeDeclaredAsPrivate - -warnif count > 0 from f in JustMyCode.Fields where - !f.IsPrivate && - // These conditions filter cases where fields - // doesn't represent state that should be encapsulated. - !f.IsGeneratedByCompiler && - !f.IsSpecialName && - !f.IsInitOnly && - !f.IsLiteral && - !f.IsEnumValue && - !f.HasAttribute("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) && - !f.HasAttribute("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) && - !f.FullName.Contains("Tests") && - !f.ParentType.Name.StartsWith("Mock") && - char.IsLower(f.Name[0]) && - !f.ParentType.Name.EndsWith("Processor") && - !f.ParentType.Name.EndsWith("Renderer") && - !f.ParentType.Name.EndsWith("Updater") && - !f.ParentType.FullName.Contains("Frameworks") && - !f.ParentType.FullName.Contains("Entities") && - !f.ParentType.FullName.Contains("Vertex") && - !f.ParentType.FullName.Contains("Vertices") && - !f.ParentType.FullName.Contains("RegisterSpriteRendererPath") && - !f.ParentType.IsInternal - -// A non-private field assigned from outside its class, -// usually leads to complicated field state management. -let outsideMethodsAssigningMe = - f.MethodsAssigningMe.Where(m => m.ParentType != f.ParentType) - -select new { - f, - f.Visibility, - outsideMethodsAssigningMe, - - Debt = (60+20*outsideMethodsAssigningMe.Count()).ToSeconds().ToDebt(), - // The cost to leave such issue unfixed is higher if the field is publicly visible! - AnnualInterest = Severity.Medium.AnnualInterestThreshold() * (f.IsPubliclyVisible ? 3 : 1) -} - -// -// This rule matches **non-private and mutable fields**. -// *Mutable* means that the field value can be modified. -// Typically mutable fields are *non-constant*, -// *non-readonly* fields. -// -// Fields should be considered as **implementation details** -// and as a consequence they should be declared as private. -// -// If something goes wrong with a *non-private field*, -// the culprit can be anywhere, and so in order to track down -// the bug, you may have to look at quite a lot of code. -// -// A private field, by contrast, can only be assigned from -// inside the same class, so if something goes wrong with that, -// there is usually only one source file to look at. -// -// Issues of this rule are fast to get fixed, and they have -// a debt proportional to the number of methods assigning -// the field. -// - -// -// Declare a matched mutable field as *private*, or declare it -// as *readonly*. -// -// If code outside the type needs to access the field -// you can encapsulate the field accesses in a read-write property. -// At least with a read-write property you can set a debug breakpoint -// on the property setter, which makes easier to track write-accesses -// in case of problem. -// -]]> - Constructors of abstract classes should be declared as protected or private -// ND1806:ConstructorsOfAbstractClassesShouldBeDeclaredAsProtectedOrPrivate - -warnif count > 0 -from t in Application.Types where - t.IsClass && - t.SimpleName != "SceneEntityRepository" && - t.IsAbstract -let ctors = t.Constructors.Where(c => !c.IsProtected && !c.IsPrivate) -where ctors.Count() > 0 -select new { - t, - ctors, - - Debt = 30.ToSeconds().ToDebt(), - Severity = Severity.Medium -} - -// -// Constructors of abstract classes can only be called from derived -// classes. -// -// Because a public constructor is creating instances of its class, -// and because it is forbidden to create instances of an *abstract* class, -// an abstract class with a public constructor is wrong design. -// -// Notice that when the constructor of an abstract class is private, -// it means that derived classes must be nested in the abstract class. -// - -// -// To fix a violation of this rule, -// either declare the constructor as *protected*, -// or do not declare the type as *abstract*. -//]]> - Avoid public methods not publicly visible -// ND1807:AvoidPublicMethodsNotPubliclyVisible - -warnif count > 0 -from m in JustMyCode.Methods where - !m.IsPubliclyVisible && m.IsPublic && - - // Eliminate virtual methods - !m.IsVirtual && - // Eliminate interface and delegate types - !m.ParentType.IsInterface && - !m.ParentType.IsDelegate && - // Eliminate default constructors - !(m.IsConstructor && m.NbParameters == 0) && - // Eliminate operators that must be declared public - !m.IsOperator && - // Eliminate methods generated by compiler, except auto-property getter/setter - (!m.IsGeneratedByCompiler || m.IsPropertyGetter || m.IsPropertySetter) && - !m.FullName.Contains("DotBulk") && - - // Don't advise to reduce visibility of property getters/setters - // of classes that implement INotifyPropertyChanged - !((m.IsPropertyGetter || m.IsPropertySetter) && - m.ParentType.Implement("System.ComponentModel.INotifyPropertyChanged".AllowNoMatch())) - -let calledOutsideParentType = - m.MethodsCallingMe.FirstOrDefault(mCaller => mCaller.ParentType != m.ParentType) != null - -select new { - m, - parentTypeVisibility = m.ParentType.Visibility, - declareMethodAs = (Visibility) (calledOutsideParentType ? Visibility.Internal : Visibility.Private), - methodsCaller = m.MethodsCallingMe, - - Debt = 30.ToSeconds().ToDebt(), - Severity = Severity.Low -} - -// -// This rule warns about methods declared as *public* -// whose parent type is not declared as *public*. -// -// In such situation *public* means, *can be accessed -// from anywhere my parent type is visible*. Some -// developers think this is an elegant language construct, -// some others find it misleading. -// -// This rule can be deactivated if you don't agree with it. -// Read the whole debate here: -// http://ericlippert.com/2014/09/15/internal-or-public/ -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -// - -// -// Declare the method as *internal* if it is used outside of -// its type, else declare it as *private*. -//]]> - Event handler methods should be declared as private or protected -// ND1808:EventHandlerMethodsShouldBeDeclaredAsPrivateOrProtected - -warnif count > 0 -from m in Application.Methods where - !(m.IsPrivate || m.IsProtected) && - !m.IsGeneratedByCompiler && - - // A method is considered as an event handler if… - m.NbParameters == 2 && // … it has two parameters … - m.Name.Contains("Object") && // … of types Object … - m.Name.Contains("EventArgs") && // … and EventArgs - - // Discard special cases - !m.ParentType.IsDelegate && - !m.IsGeneratedByCompiler - -select new { - m, - m.Visibility, - Debt = 2.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Think of a event handler like for example *OnClickButton()*. -// Typically such method must be declared as *private* -// and shouldn't be called in other context than event firing. -// -// Such method can also be declared as *protected* because -// some designers such as the ASP.NET designer, generates such method -// as *protected* to let a chance to sub-classes to call it. -// - -// -// If you have the need that event handler method should be called -// from another class, then find a code structure that more -// closely matches the concept of what you're trying to do. -// Certainly you don't want the other class to click a button; you -// want the other class to do something that clicking a button -// also do. -//]]> - Wrong usage of CannotDecreaseVisibilityAttribute -// ND1809:WrongUsageOfCannotDecreaseVisibilityAttribute - -warnif count > 0 - -let tAttr = Types.WithFullName("NDepend.Attributes.CannotDecreaseVisibilityAttribute").FirstOrDefault() -where tAttr != null - -// Get types that do a wrong usage of CannotDecreaseVisibilityAttribute -let types = from t in Application.Types where - t.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) && - ( t.Visibility == t.OptimalVisibility || - - // Static types that define only const fields cannot be seen as used in IL code. - // They don't need to be tagged with CannotDecreaseVisibilityAttribute. - (t.IsStatic && t.NbMethods == 0 && !t.Fields.Any(f => !f.IsLiteral)) - ) - select t - -// Get methods that do a wrong usage of CannotDecreaseVisibilityAttribute -let methods = from m in Application.Methods where - m.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) && - m.Visibility == m.OptimalVisibility - select m - -// Get fields that do a wrong usage of CannotDecreaseVisibilityAttribute -let fields = from f in Application.Fields where - f.HasAttribute("NDepend.Attributes.CannotDecreaseVisibilityAttribute".AllowNoMatch()) && - f.Visibility == f.OptimalVisibility - select f - -from member in types.Cast().Concat(methods).Concat(fields) -select new { - member, - Debt = 30.ToSeconds().ToDebt(), - Severity = Severity.Low -} - -// -// The attribute **NDepend.Attributes.CannotDecreaseVisibilityAttribute** -// is defined in *NDepend.API.dll*. If you don't want to reference -// *NDepend.API.dll* you can declare it in your code. -// -// Usage of this attribute means that a code element visibility is not -// optimal (it can be lowered like for example from *public* to *internal*) -// but shouldn’t be modified anyway. Typical reasons to do so include: -// -// • Public code elements consumed through reflection, through a mocking -// framework, through XML or binary serialization, through designer, -// COM interfaces… -// -// • Non-private code element invoked by test code, that would be difficult -// to reach from test if it was declared as *private*. -// -// In such situation *CannotDecreaseVisibilityAttribute* is used to avoid -// that default rules about not-optimal visibility warn. Using this -// attribute can be seen as an extra burden, but it can also be seen as -// an opportunity to express in code: **Don't change the visibility else -// something will be broken** -// -// In this context, this code rule matches code elements -// (types, methods, fields) that are tagged with this attribute, -// but still already have an optimal visibility. -// - -// -// Just remove *CannotDecreaseVisibilityAttribute* tagging of -// types, methods and fields matched by this rule -// because this tag is not useful anymore. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - Methods that should be declared as 'public' in C#, 'Public' in VB.NET - -from m in Application.Methods where - m.ShouldBePublic -let usedInAssemblies = m.MethodsCallingMe.ParentAssemblies().Except(m.ParentAssembly) -select new { - m, - m.ParentAssembly, - usedInAssemblies, - m.MethodsCallingMe -} - -// -// This code query lists methods that *should* be declared -// as *public*. Such method is actually declared as *internal* -// and is consumed from outside its parent assembly -// thanks to the attribute -// *System.Runtime.CompilerServices.InternalsVisibleToAttribute*. -// -// This query relies on the property -// *NDepend.CodeModel.IMember.ShouldBePublic* -// https://www.ndepend.com/api/webframe.html?NDepend.API~NDepend.CodeModel.IMember~ShouldBePublic.html -// -// This is just a code query, it is not intended to advise -// you to declare the method as *public*, but to inform you -// that the code actually relies on the peculiar behavior -// of the attribute *InternalsVisibleToAttribute*. -//]]> - - - Fields should be marked as ReadOnly when possible -// ND1900:FieldsShouldBeMarkedAsReadOnlyWhenPossible - -warnif count > 0 -from f in JustMyCode.Fields where - f.IsImmutable && - f.Parent.Name != "VertexNativeData" && - f.Parent.Name != "VorbisTime" && - !f.IsInitOnly && // The condition IsInitOnly matches fields that - // are marked with the C# readonly keyword - // (ReadOnly in VB.NET). - !f.IsGeneratedByCompiler && - !f.IsEventDelegateObject && - !f.FullName.Contains("Wgl") && - !f.FullName.Contains("ForwardKinematics") && - !f.FullName.Contains("Create3DPointCloud") && - !f.FullName.Contains("VideoInput") && - !f.ParentType.IsEnumeration && - !f.IsLiteral && - - // Don't warn if a method using the field is also calling a method that has 'ref' and 'out' parameters. - // This could lead to false positive. A field used in a 'ref' or 'out' parameter cannot be set as read-only. - f.MethodsUsingMe.SelectMany(m => m.MethodsCalled).FirstOrDefault(m => m.Name.Contains("&")) == null - -select new { - f, - f.MethodsReadingMeButNotAssigningMe, - f.MethodsAssigningMe, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about instance and static fields that -// can be declared as **readonly**. -// -// This source code of this rule is based on the conditon -// *IField.IsImmutable*. -// https://www.ndepend.com/api/webframe.html?NDepend.API~NDepend.CodeModel.IField~IsImmutable.html -// -// A field that matches the condition *IsImmutable* -// is a field that is assigned only by constructors -// of its class. -// -// For an *instance field*, this means its value -// will remain constant through the lifetime -// of the object. -// -// For a *static field*, this means its value will -// remain constant through the lifetime of the -// program. -// - -// -// Declare the field with the C# *readonly* keyword -// (*ReadOnly* in VB.NET). This way the intention -// that the field value shouldn't change is made -// explicit. -//]]> - Avoid non-readonly static fields -// ND1901:AvoidNonReadOnlyStaticFields - -warnif count > 0 -from f in Application.Fields -where f.IsStatic && - !f.IsEnumValue && - !f.IsGeneratedByCompiler && - !f.IsLiteral && - !f.IsInitOnly && - !f.IsPublic && - !f.FullName.Contains("Scene") && - !f.FullName.Contains("Resolver") && - !f.ParentType.Name.EndsWith("Client") && - !f.Name.Contains("wasStarted") && - !f.Name.Contains("threadStatics") && - f.Name != "IsColorAndVectorInitialized" - -let methodAssigningField = f.MethodsAssigningMe - -select new { - f, - methodAssigningField, - Debt = (2+8*methodAssigningField.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns about static fields that are not -// declared as read-only. -// -// In *Object-Oriented-Programming* the natural artifact -// to hold states that can be modified is **instance fields**. -// Such mutable static fields create *confusion* about -// the expected state at runtime and impairs the code -// testability since the same mutable state is re-used for -// each test. -// -// More discussion on the topic can be found here: -// http://codebetter.com/patricksmacchia/2011/05/04/back-to-basics-usage-of-static-members/ -// - -// -// If the *static* field is just assigned once in the program -// lifetime, make sure to declare it as *readonly* and assign -// it inline, or from the static constructor. -// -// Else if methods other than the static constructor need to -// assign the state hold by the static field, refactoring must -// occur to ensure that this state is hold through an instance -// field. -//]]> - Avoid static fields with a mutable field type -// ND1902:AvoidStaticFieldsWithAMutableFieldType - -warnif count > 0 -from f in Application.Fields -where f.IsStatic && - !f.IsEnumValue && - !f.IsGeneratedByCompiler && - !f.IsLiteral - -let fieldType = f.FieldType -where fieldType != null && - !fieldType.Name.Contains("Vertex") && - !f.FullName.Contains("Mock") && - !f.FullName.Contains("UVRectangle") && - !f.Name.StartsWith("current") && - f.Name != "Shared" && - f.Name != "selectedEditModeEntity" && - f.Name != "sharedTooltip" && - f.ParentType.Name != "GLCore" && - fieldType.Name == "ThreadStatic" && - !fieldType.IsThirdParty && - !fieldType.IsInterface && - !fieldType.IsImmutable && - !fieldType.Name.Contains("Vertex") && - !f.FullName.Contains("Mock") && - !f.FullName.Contains("UVRectangle") && - !f.Name.StartsWith("current") && - f.Name != "Shared" && - !fieldType.Name.Contains("ThreadStatic") && - f.Name != "selectedEditModeEntity" && - f.Name != "sharedTooltip" && - f.ParentType.Name != "GLCore" - -select new { - f, - mutableFieldType = fieldType, - isFieldImmutable = f.IsImmutable ? "Immutable" : "Mutable", - isFieldReadOnly = f.IsInitOnly ? "ReadOnly" : "Not ReadOnly", - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about static fields whose field type -// is mutable. In such case the static field is -// holding a state that can be modified. -// -// In *Object-Oriented-Programming* the natural artifact -// to hold states that can be modified is **instance fields**. -// Hence such static fields create *confusion* about -// the expected state at runtime. -// -// More discussion on the topic can be found here: -// http://codebetter.com/patricksmacchia/2011/05/04/back-to-basics-usage-of-static-members/ -// - -// -// To fix violations of this rule, make sure to -// hold mutable states through objects that are passed -// **explicitly** everywhere they need to be consumed, in -// opposition to mutable object hold by a static field that -// makes it modifiable from a bit everywhere in the program. -//]]> - Structures should be immutable -// ND1903:StructuresShouldBeImmutable - -warnif count > 0 from t in JustMyCode.Types where - t.IsStructure && - !t.Name.Contains("Native") && - !t.Name.Contains("Vertex") && - !t.FullName.Contains("NVorbis") && - !t.Name.Contains("ParentProcess") && - !t.FullName.Contains("Frameworks.") && - !t.FullName.Contains("Input") && - !t.FullName.Contains("Test") && - !t.FullName.Contains("+<") && - !t.FullName.Contains("ImageProcessing.ColorFilters") && - !t.IsImmutable && - t.Name != "Pixel" - -let mutableFields = t.Fields.Where(f => !f.IsImmutable) - -select new { - t, - t.NbLinesOfCode, - mutableFields, - Debt = (3+2*mutableFields.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// An object is immutable if its state doesn’t change once the -// object has been created. Consequently, a structure or a class -// is immutable if its instances fields are only assigned inline -// or from constructor(s). -// -// But for structure it is a bit different. **Structures are value -// types** which means instances of structures are copied when they are -// passed around (like through a method argument). -// -// So if you change a copy you are changing only that copy, not the -// original and not any other copies which might be around. Such -// situation is very different than what happen with instances of -// classes. Hence developers are not used to work with modified values -// and doing so introduces *confusion* and is *error-prone*. -// - -// -// Make sure matched structures are immutable. This way, all -// automatic copies of an original instance, resulting from being -// *passed by value* will hold the same values and there will be -// no surprises. -// -// If your structure is immutable then if you want to change -// a value, you have to consciously do it by creating a new instance -// of the structure with the modified data. -//]]> - Property Getters should be immutable -// ND1904:PropertyGettersShouldBeImmutable - -warnif count > 0 from m in Application.Methods where - !m.IsGeneratedByCompiler && - m.IsPropertyGetter && - !m.FullName.Contains("SystemInformation") && - !m.FullName.Contains("Scene") && - ( ( !m.IsStatic && m.ChangesObjectState) || - ( m.IsStatic && m.ChangesTypeState) ) - -let propertyName = m.SimpleName.Substring(4,m.SimpleName.Length-4) -let setterSimpleName = "set_" + propertyName - - -let fieldsAssigned = m.FieldsAssigned.Where(f => - - // Don't count field that have a name similar to the property name - // to avoid matching lazy initialization situations. - !(propertyName.Length >= 4 && - f.SimpleName.Length >= 4 && - // Don't count the first 3 characters of the field name, - // to avoid special field name formatting like 'm_X' or '_x' - propertyName.EndsWith(f.SimpleName.Substring(3, f.SimpleName.Length -3))) - && - - // Don't count field that are assigned only by the property getter and the related property setter. - f.MethodsAssigningMe.Any(m1 => m1 != m && - !(m1.IsPropertySetter && m1.SimpleName == setterSimpleName))) -where fieldsAssigned.Any() -let otherMethodsAssigningSameFields = fieldsAssigned.SelectMany(f => f.MethodsAssigningMe.Where(m1 => m1 != m)) - -select new { - m, - m.NbLinesOfCode, - fieldsAssigned, - otherMethodsAssigningSameFields, - Debt = (2 + 5*fieldsAssigned.Count() + 5*otherMethodsAssigningSameFields.Count() ).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// It is not expected that a state gets modified when -// accessing a property getter. Hence doing so create -// confusion and property getters should be pure methods, -// they shouldn't assign any field. -// -// This rule doesn't match property getters that assign a field -// not assigned by any other methods than the getter itself -// and the corresponding property setter. Hence this rule avoids -// matching *lazy initialization at first access* of a state. -// In such situation the getter assigns a field at first access -// and from the client point of view, lazy initialization -// is an invisible implementation detail. -// -// A field assigned by a property with a name similar to the -// property name are not count either, also to avoid matching -// *lazy initialization at first access* situations. -// - -// -// Make sure that matched property getters don't assign any -// field. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 2 minutes plus 5 minutes per field assigned and -// 5 minutes per other method assigning such field. -//]]> - A field must not be assigned from outside its parent hierarchy types -// ND1905:AFieldMustNotBeAssignedFromOutsideItsParentHierarchyTypes - -warnif count > 0 -from f in JustMyCode.Fields.Where(f => - (f.IsInternal || f.IsPublic) && - !f.IsGeneratedByCompiler && - !f.IsImmutable && - !f.IsEnumValue && - !f.FullName.Contains("Test") && - !f.FullName.Contains("Rendering") && - !f.FullName.Contains("Graphics") && - !f.FullName.Contains("Glyph") && - !f.FullName.Contains("Tcp") && - !f.FullName.Contains("NVorbis") && - !f.FullName.Contains("Pathfinding") && - !f.FullName.Contains("VideoInputs") && - !f.FullName.Contains("Strict.ObjectDetection.BoundingBox") && - !f.FullName.Contains("Strict.ObjectDetection.DotBulk") && - !f.FullName.Contains("Strict.ObjectDetection.Pixel") && - !f.FullName.Contains("Strict.Robotics.Kinematics.AxisParameters") && - f.Name != "usedVertices" && - f.Name != "usedIndices" && - f.Name != "NumberOfVertices" && - f.Name != "NumberOfIndices" && - f.Name != "array" && - f.Name != "madeApprovalImage" && - f.Name != "CodeAfterFirstFrameAndFirstTick") - -let methodsAssignerOutsideOfMyType = f.MethodsAssigningMe.Where( - m =>!m.IsGeneratedByCompiler && - m.ParentType != f.ParentType && - !m.ParentType.DeriveFrom(f.ParentType) ) - -where methodsAssignerOutsideOfMyType.Any() - -select new { - f, - methodsAssignerOutsideOfMyType, - Debt = (5*methodsAssignerOutsideOfMyType.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule is related to the rule *Fields should be declared as -// private*. It matches any **public or internal, mutable field** -// that is assigned from outside its parent class and subclasses. -// -// Fields should be considered as **implementation details** -// and as a consequence they should be declared as private. -// -// If something goes wrong with a *non-private field*, -// the culprit can be anywhere, and so in order to track down -// the bug, you may have to look at quite a lot of code. -// -// A private field, by contrast, can only be assigned from -// inside the same class, so if something goes wrong with that, -// there is usually only one source file to look at. -// - -// -// Matched fields must be declared as *protected* and even better -// as *private*. -// -// Alternatively, if the field can reference immutable states, -// it can remain visible from the outside, but then must be -// declared as *readonly*. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 5 minutes per method outside the parent hierarchy -// that assigns the matched field. -//]]> - Don't assign a field from many methods -// ND1906:DontAssignAFieldFromManyMethods - -warnif count > 0 -from f in JustMyCode.Fields where - !f.Name.Contains("test") && - !f.Name.Contains("Test") && - !f.IsEnumValue && - !f.IsImmutable && - !f.IsInitOnly && - !f.IsGeneratedByCompiler && - !f.IsEventDelegateObject && - f.Name != "currentDataLength" && - f.Name != "currentLengthMode" && - f.Name != "nestingDepth" && - f.Name != "offset" && - f.Name != "wasDataChanged" && - f.Name != "wasChangedThisFrame" && - f.Name != "isCreated" && - !f.FullName.Contains("NVorbis") && - f.Name != "isCreated" && - f.Name != "numberOfBytesAvailable" && - f.Name != "connectionError" && - f.Name != "currentTextLineWidth" && - f.Name != "frameDownCounter" && - !f.Name.StartsWith("cache") && - f.Name != "shader" && - f.Name != "window" && - f.Name != "thisFrameTicks" && - f.Name != "testOrProjectName" && - f.Name != "unitTestClassFullName" && - f.Name != "entities" && - f.Name != "scenes" && - f.Name != "index" && - f.Name != "position" && - f.Name != "isPaused" && - f.Name != "hasParents" && - f.Name != "rememberDesktopDisplaySizeBeforeFullscreen" && - f.Name != "inSwitchingFullscreenMode" && - f.ParentType.Name != "CommonInputTriggers" && - !f.ParentType.Name.EndsWith("Tests") && - (f.IsPrivate || f.IsProtected) // Don't match fields assigned outside their classes. - -let methodsAssigningMe = f.MethodsAssigningMe.Where(m => !m.IsConstructor) - -// The threshold 4 is arbitrary and it should avoid matching too many fields. -// Threshold is even lower for static fields because this reveals situations even more complex. -where methodsAssigningMe.Count() >= (!f.IsStatic ? 4 : 2) - -select new { - f, - methodsAssigningMe, - f.MethodsReadingMeButNotAssigningMe, - f.MethodsUsingMe, - Debt = (4+(f.IsStatic ? 10 : 5)).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// A field assigned from many methods is a symptom of **bug-prone code**. -// Notice that: -// -// • For an instance field, constructor(s) of its class that assign the field are not counted. -// -// • For a static field, the class constructor that assigns the field is not counted. -// -// The default threshold for *instance fields* is equal to *4 or more than 4 methods -// assigning the instance field*. Such situation makes harder to anticipate the -// field state at runtime. The code is then complicated to read, hard to debug -// and hard to maintain. Hard-to-solve bugs due to corrupted state are often the -// consequence of fields *anarchically assigned*. -// -// The situation is even more complicated if the field is *static*. -// Indeed, such situation potentially involves global random accesses from -// various parts of the application. This is why this rule provides a lower -// threshold equals to *2 or more than 2 methods assigning the static field*. -// -// If the object containing such field is meant to be used from multiple threads, -// there are **alarming chances** that the code is unmaintainable and bugged. -// When multiple threads are involved, the rule of thumb is to use immutable objects. -// -// If the field type is a reference type (interfaces, classes, strings, delegates) -// corrupted state might result in a *NullReferenceException*. -// If the field type is a value type (number, boolean, structure) -// corrupted state might result in wrong result not even signaled by an exception -// thrown. -// - -// -// There is no straight advice to refactor the number of methods responsible -// for assigning a field. Sometime the situation is simple enough, like when -// a field that hold an indentation state is assigned by many writer methods. -// Such situation only requires to define two methods *IndentPlus()/IndentMinus()* -// that assign the field, called from all writers methods. -// -// Sometime the solution involves rethinking and then rewriting -// a complex algorithm. Such field can sometime become just a variable accessed -// locally by a method or a *closure*. Sometime, just rethinking the life-time -// and the role of the parent object allows the field to become immutable -// (i.e assigned only by the constructor). -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 4 minutes plus 5 minutes per method assigning the instance field -// or 10 minutes per method assigning the static field. -//]]> - Do not declare read only mutable reference types -// ND1907:DoNotDeclareReadOnlyMutableReferenceTypes - -warnif count > 0 -from f in JustMyCode.Fields where - f.IsInitOnly && - f.FieldType != null && - f.FieldType.IsClass && - f.Name != "Shared" && - !f.ParentType.IsPrivate && - !f.IsPrivate && - !f.IsProtected && - f.FieldType != null && - f.FieldType.IsClass && - !f.FieldType.IsThirdParty && - !f.FieldType.IsImmutable && - f.Name != "Shared" && - f.ParentType.Name != "VertexBatch" && - !f.FullName.Contains("Entities") && - !f.FullName.Contains("Content") && - !f.FullName.Contains("Vertices") && - !f.FullName.Contains("Input") && - !f.FullName.Contains("Renderer") -select new { - f, - f.FieldType, - FieldVisibility = f.Visibility, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This rule is violated when a *public* or *internal* -// type contains a *public* or *internal* read-only field -// whose field type is a mutable reference type. -// -// This situation gives the false impression that the -// value can't change, when actually it's only the field -// value that can't change, but the object state -// can still change. -// - -// -// To fix a violation of this rule, -// replace the field type with an immutable type, -// or declare the field as *private*. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - Array fields should not be read only -// ND1908:ArrayFieldsShouldNotBeReadOnly - -warnif count > 0 -from f in Application.Fields where - f.IsInitOnly && - f.IsPubliclyVisible && - f.FieldType != null && - f.FieldType.FullName == "System.Array" && - !f.Name.Contains("Default") && - !f.Name.Contains("Indices") && - !f.Name.Contains("States") && - !f.Name.Contains("Positions") && - !f.Name.Contains("UVs") && - !f.Name.Contains("Vertices") && - !f.Name.Contains("Coords") && - !f.ParentType.Name.StartsWith("TouchStates") -select new { - f, - FieldVisibility = f.Visibility, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This rule is violated when a publicly visible field -// that holds an array, is declared read-only. -// -// This situation represents a *security vulnerability*. -// Because the field is read-only it cannot be changed to refer -// to a different array. However, the elements of the array -// that are stored in a read-only field can be changed. -// Code that makes decisions or performs operations that are -// based on the elements of a read-only array that can be publicly -// accessed might contain an exploitable security vulnerability. -// - -// -// To fix the security vulnerability that is identified by -// this rule do not rely on the contents of a read-only array -// that can be publicly accessed. It is strongly recommended -// that you use one of the following procedures: -// -// • Replace the array with a strongly typed collection -// that cannot be changed. See for example: -// *System.Collections.Generic.IReadOnlyList* ; -// *System.Collections.Generic.IReadOnlyCollection* ; -// *System.Collections.ReadOnlyCollectionBase* -// -// • Or replace the public field with a method that returns a clone -// of a private array. Because your code does not rely on -// the clone, there is no danger if the elements are modified. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - Types tagged with ImmutableAttribute must be immutable -// ND1909:TypesTaggedWithImmutableAttributeMustBeImmutable - -warnif count > 0 -from t in Application.Types where - t.HasAttribute ("NDepend.Attributes.ImmutableAttribute".AllowNoMatch()) && - !t.IsImmutable -let culpritFields = t.Fields.Where(f => !f.IsStatic && !f.IsImmutable) -select new { - t, - culpritFields, - Debt = (5+10*culpritFields.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// An object is immutable if its state doesn’t change once the -// object has been created. Consequently, a structure or a class -// is immutable if its instances fields are only assigned inline -// or from constructor(s). -// -// An attribute **NDepend.Attributes.ImmutableAttribute** can be -// used to express in code that a type is immutable. In such -// situation, the present code rule checks continuously that the -// type remains immutable whatever the modification done. -// -// This rule warns when a type that is tagged with -// *ImmutableAttribute* is actually not immutable anymore. -// -// Notice that *FullCoveredAttribute* is defined in *NDepend.API.dll* -// and if you don't want to link this assembly, you can create your -// own *FullCoveredAttribute* and adapt the rule. -// - -// -// First understand which modification broke the type immutability. -// The list of *culpritFields* provided in this rule result can help. -// Then try to refactor the type to bring it back to immutability. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 5 minutes plus 10 minutes per culprit field. -//]]> - Types immutable should be tagged with ImmutableAttribute -// ND1910:TypesImmutableShouldBeTaggedWithImmutableAttribute - -// warnif count > 0 <-- not a code rule per default - -from t in Application.Types where - !t.HasAttribute ("NDepend.Attributes.ImmutableAttribute".AllowNoMatch()) && - t.IsImmutable -select new { - t, - t.NbLinesOfCode, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// An object is immutable if its state doesn’t change once the -// object has been created. Consequently, a structure or a class -// is immutable if its instances fields are only assigned inline -// or from constructor(s). -// -// This code query lists immutable type that are not tagged with -// an **ImmutableAttribute**. By using such attribute, you can express -// in source code the intention that a class is immutable, and -// should remain immutable in the future. Benefits of using -// an **ImmutableAttribute** are twofold: -// -// • Not only the intention is expressed in source code, -// -// • but it is also continuously checked by the rule -// *Types tagged with ImmutableAttribute must be immutable*. -// - -// -// Just tag types matched by this code query with **ImmutableAttribute** -// that can be found in *NDepend.API.dll*, -// or by an attribute of yours defined in your own code -// (in which case this code query must be adapted). -//]]> - Methods tagged with PureAttribute must be pure -// ND1911:MethodsTaggedWithPureAttributeMustBePure - -warnif count > 0 -from m in Application.Methods where - ( m.HasAttribute ("NDepend.Attributes.PureAttribute".AllowNoMatch()) || - m.HasAttribute ("System.Diagnostics.Contract.PureAttribute".AllowNoMatch()) ) && - ( m.ChangesObjectState || m.ChangesTypeState ) && - m.NbLinesOfCode > 0 - -let fieldsAssigned = m.FieldsAssigned - -select new { - m, - m.NbLinesOfCode, - fieldsAssigned, - Debt = 15.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// A method is pure if its execution doesn’t change -// the value of any instance or static field. -// A pure method is just a **function** that output -// a result from inputs. -// Pure methods naturally simplify code by **limiting -// side-effects**. -// -// An attribute **PureAttribute** can be -// used to express in code that a method is pure. In such -// situation, the present code rule checks continuously that the -// method remains pure whatever the modification done. -// -// This rule warns when a method that is tagged with -// *PureAttribute* is actually not pure anymore. -// -// Notice that *NDepend.Attributes.PureAttribute* is defined -// in *NDepend.API.dll* and if you don't want to link this -// assembly, you can also use -// *System.Diagnostics.Contract.PureAttribute* -// or create your own *PureAttribute* and adapt the rule. -// -// Notice that *System.Diagnostics.Contract.PureAttribute* is -// taken account by the compiler only when the VS project has -// Microsoft Code Contract enabled. -// - -// -// First understand which modification broke the method purity. -// Then refactor the method to bring it back to purity. -//]]> - Pure methods should be tagged with PureAttribute -// ND1912:PureMethodsShouldBeTaggedWithPureAttribute - -// warnif count > 0 <-- not a code rule per default - -from m in Application.Methods where - !m.IsGeneratedByCompiler && - !m.HasAttribute ("NDepend.Attributes.PureAttribute".AllowNoMatch()) && - !m.HasAttribute ("System.Diagnostics.Contract.PureAttribute".AllowNoMatch()) && - !m.ChangesObjectState && !m.ChangesTypeState && - m.NbLinesOfCode > 0 -select new { - m, - m.NbLinesOfCode, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// A method is pure if its execution doesn’t change -// the value of any instance or static field. -// Pure methods naturally simplify code by **limiting -// side-effects**. -// -// This code query lists pure methods that are not tagged with -// a **PureAttribute**. By using such attribute, you can express -// in source code the intention that a method is pure, and -// should remain pure in the future. Benefits of using -// a **PureAttribute** are twofold: -// -// • Not only the intention is expressed in source code, -// -// • but it is also continuously checked by the rule -// *Methods tagged with PureAttribute must be pure*. -// -// This code query is not by default defined as a code rule -// because certainly many of the methods of the code base -// are matched. Hence fixing all matches and then -// maintaining the rule unviolated might require a lot of -// work. This may *counter-balance* such rule benefits. -// - -// -// Just tag methods matched by this code query with -// *NDepend.Attributes.PureAttribute* -// that can be found in *NDepend.API.dll*, -// or with *System.Diagnostics.Contract.PureAttribute*, -// or with an attribute of yours defined in your own code -// (in which case this code query must be adapted). -// -// Notice that *System.Diagnostics.Contract.PureAttribute* is -// taken account by the compiler only when the VS project has -// Microsoft Code Contract enabled. -//]]> - - - Instance fields naming convention -// ND2000:InstanceFieldsNamingConvention - -warnif count > 0 from f in JustMyCode.Fields where - !( - // Instance field name starting with a lower-case letter - (f.Name.Length >= 1 && char.IsLetter(f.Name[0]) && char.IsLower(f.Name[0])) || - - // Instance field name starting with "_" followed with a lower-case letter - (f.Name.Length >= 2 && f.Name.StartsWith("_") && char.IsLetter(f.Name[1]) && char.IsLower(f.Name[1])) || - - // Instance field name starting with "m_" followed with an upper-case letter - (f.Name.Length >= 3 && f.Name.StartsWith("m_") && char.IsLetter(f.Name[2]) && char.IsUpper(f.Name[2])) - - ) && - f.Name != "X" && f.Name != "Y" && f.Name != "Z" && - f.Name != "R" && f.Name != "G" && f.Name != "B" && f.Name != "A" && - !f.IsStatic && - !f.IsLiteral && - !f.IsGeneratedByCompiler && - !f.IsSpecialName && - !f.IsEventDelegateObject && - - !f.FullName.Contains("Datatypes") && - !f.FullName.Contains("Entities") && - !f.FullName.Contains("Xml") && - !f.FullName.Contains("Content") && - !f.FullName.Contains("Graphics") && - !f.FullName.Contains("Sprites") && - !f.FullName.Contains("Fonts") && - !f.FullName.Contains("Shapes") && - !f.FullName.Contains("Resolvers") && - !f.FullName.Contains("Mocks") && - !f.FullName.Contains("NVorbis") && - !f.FullName.Contains("VideoInputs") && - !f.FullName.Contains("Scene+") && - !f.FullName.Contains("Strict.ObjectDetection.Pixel.Grey") && - !f.FullName.Contains("ThreeDimensionalPointCloud") && - - // Don't check naming convention on serializable fields - !f.HasAttribute("System.Xml.Serialization.XmlAttributeAttribute".AllowNoMatch()) && - !f.HasAttribute("System.Runtime.Serialization.DataMemberAttribute".AllowNoMatch()) && - - // Don't warn if a method using the field is also calling a method that has 'ref' and 'out' parameters. - // This could lead to false positive. A field used in a 'ref' or 'out' parameter cannot be set as read-only. - f.MethodsUsingMe.SelectMany(m => m.MethodsCalled).FirstOrDefault(m => m.Name.Contains("&")) == null - -select new { - f, - f.SizeOfInst, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// By default the presents rule supports the 3 most used naming -// conventions for instance fields: -// -// • Instance field name starting with a lower-case letter -// -// • Instance field name starting with "_" followed with a lower-case letter -// -// • Instance field name starting with "m_" followed with an upper-case letter -// -// The rule can be easily adapted to your own company naming convention. -// -// In terms of behavior, a *static field* is something completely different -// than an *instance field*, so it is interesting to differentiate them at -// a glance through a naming convetion. -// -// This is why it is advised to use a specific naming convention for instance -// field like name that starts with **m_**. -// -// Related discussion: -// http://codebetter.com/patricksmacchia/2013/09/04/on-hungarian-notation-for-instance-vs-static-fields-naming/ -// - -// -// Once the rule has been adapted to your own naming convention -// make sure to name all matched instance fields adequately. -//]]> - Static fields naming convention -// ND2001:StaticFieldsNamingConvention - -warnif count > 0 from f in JustMyCode.Fields where - !( - // Static field name starting with an upper-case letter - (f.Name.Length >= 1 && char.IsLetter(f.Name[0]) && char.IsUpper(f.Name[0])) || - - // Static field name starting with "_" followed with an upper-case letter - (f.Name.Length >= 2 && f.Name.StartsWith("_") && char.IsLetter(f.Name[1]) && char.IsUpper(f.Name[1])) || - - // Static field name starting with "s_" followed with an upper-case letter - (f.Name.Length >= 3 && f.Name.StartsWith("s_") && char.IsLetter(f.Name[2]) && char.IsUpper(f.Name[2])) - - ) && - !f.Name.Contains("test") && - !f.Name.Contains("Test") && - !f.Name.Contains("wasStarted") && - !f.Name.Contains("threadStatics") && - f.IsStatic && - !f.IsLiteral && - !f.IsGeneratedByCompiler && - !f.IsSpecialName && - !f.IsEventDelegateObject && - !f.FullName.Contains("Scene") && - !f.FullName.Contains("GLCore") && - !f.FullName.Contains("BinaryData") && - !f.Parent.FullName.EndsWith("Extensions") && - !f.FullName.Contains("TcpClient") && - !f.FullName.Contains("MockServerClient") && - !f.FullName.Contains("Resolver") && - !f.FullName.Contains("ThreadStatic") && - !f.ParentNamespace.Name.Contains("NVorbis") -select new { - f, - f.SizeOfInst, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// By default the presents rule supports the 3 most used naming -// conventions for static fields: -// -// • Static field name starting with an upper-case letter -// -// • Static field name starting with "_" followed with an upper-case letter -// -// • Static field name starting with "s_" followed with an upper-case letter -// -// The rule can be easily adapted to your own company naming convention. -// -// In terms of behavior, a *static field* is something completely different -// than an *instance field*, so it is interesting to differentiate them at -// a glance through a naming convetion. -// -// This is why it is advised to use a specific naming convention for static -// field like name that starts with **s_**. -// -// Related discussion: -// http://codebetter.com/patricksmacchia/2013/09/04/on-hungarian-notation-for-instance-vs-static-fields-naming/ -// - -// -// Once the rule has been adapted to your own naming convention -// make sure to name all matched static fields adequately. -//]]> - Interface name should NOT begin with a 'I' -// ND2002:InterfaceNameShouldBeginWithAI - -// Don't query all Application.Types because some interface generated might not start with an 'I' -// like for example when adding a WSDL reference to a project. -warnif count > 0 from t in JustMyCode.Types where - t.IsInterface && - !t.FullName.StartsWith("NVorbis") && - !t.FullName.Contains("WebCam.Internal") && - // Don't apply this rule for COM interfaces. - !t.HasAttribute("System.Runtime.InteropServices.ComVisibleAttribute".AllowNoMatch()) - -// Discard outter type(s) name prefix for nested types -let name = !t.IsNested ? - t.Name : - t.Name.Substring(t.Name.LastIndexOf('+') + 1, t.Name.Length - t.Name.LastIndexOf('+') - 1) - -where name[0] == 'I' && char.IsUpper(name[1]) -select new { - t, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// In the .NET world, interfaces names are commonly prefixed -// with an upper case **I**. This rule warns about interfaces -// whose names don't follow this convention. Because this -// naming convention is widely used and accepted, we -// recommend abiding by this rule. -// -// Typically COM interfaces names don't follow this rule. -// Hence this code rule doesn't take care of interfaces tagged -// with *ComVisibleAttribute*. -// - -// -// Make sure that matched interfaces names are prefixed with -// an upper **I**. -//]]> - Abstract base class should NOT be suffixed with 'Base' -// ND2003:AbstractBaseClassShouldBeSuffixedWithBase - -warnif count > 0 from t in Application.Types where - t.IsAbstract && - t.IsClass && - - t.BaseClass != null && - t.BaseClass.FullName == "System.Object" && - - ((!t.IsGeneric && t.NameLike (@"Base$")) || - ( t.IsGeneric && t.NameLike (@"Base<"))) -select new { - t, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about *abstract classes* whose names are not -// suffixed with **Base**. It is a common practice in the .NET -// world to suffix base classes names with **Base**. -// -// Notice that this rule doesn't match abstract classes that -// are in a middle of a hierarchy chain. -// In other words, only base classes that derive directly -// from *System.Object* are matched. -// - -// -// Suffix the names of matched base classes with **Base**. -//]]> - Exception class name should NOT be suffixed with 'Exception' -// ND2004:ExceptionClassNameShouldBeSuffixedWithException - -warnif count > 0 from t in Application.Types where - t.IsExceptionClass && - !t.FullName.Contains("XmlData") && - !t.FullName.Contains("NetworkingException") && - !t.FullName.Contains("DeviceNotFoundException") && - // We use SimpleName, because in case of generic Exception type - // SimpleName suppresses the generic suffix (like ). - t.SimpleNameLike(@"Exception$") - -select new { - t, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns about *exception classes* whose names are not -// suffixed with **Exception**. It is a common practice in the .NET -// world to suffix exception classes names with **Exception**. -// -// For exception base classes, the suffix **ExceptionBase** -// is also accepted. -// - -// -// Suffix the names of matched exception classes with **Exception**. -//]]> - Attribute class name should be suffixed with 'Attribute' -// ND2005:AttributeClassNameShouldBeSuffixedWithAttribute - -warnif count > 0 from t in Application.Types where - t.IsAttributeClass && - !t.NameLike (@"Attribute$") -select new { - t, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns about *attribute classes* whose names are not -// suffixed with **Attribute**. It is a common practice in the .NET -// world to suffix attribute classes names with **Attribute**. -// - -// -// Suffix the names of matched attribute classes with **Attribute**. -//]]> - Types name should begin with an Upper character -// ND2006:TypesNameShouldBeginWithAnUpperCharacter - -warnif count > 0 -let isAspNetApp = ThirdParty.Assemblies.WithName("System.Web").Any() -from t in JustMyCode.Types where - // The name of a type should begin with an Upper letter. - !t.SimpleNameLike (@"^[A-Z]") && - !t.NameLike("GLCore") && - !t.NameLike("WglGraphicsContext") && - // Except if it is generated by compiler. - !t.IsSpecialName && - !t.IsGeneratedByCompiler && - - // Special default ASP.NET type named "_Default" - !(isAspNetApp && t.SimpleName == "_Default") - -select new { - t, - // We show the type simple name - // that doesn't include the parent type name - // for nested types. - t.SimpleName, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about *types* whose names don't start -// with an Upper character. It is a common practice in the .NET -// world to use **Pascal Casing Style** to name types. -// -// **Pascal Casing Style** : The first letter in the identifier -// and the first letter of each subsequent concatenated word -// are capitalized. For example: *BackColor* -// - -// -// *Pascal Case* the names of matched types. -//]]> - Methods name should begin with an Upper character -// ND2007:MethodsNameShouldBeginWithAnUpperCharacter - -warnif count > 0 -from m in JustMyCode.Methods where - !m.NameLike (@"^[A-Z]") && - !m.IsSpecialName && - m.Name != "waveOutGetNumDevs()" && - m.Name != "joyGetDevCaps(UInt32,WindowsGamePad+JoyCaps&,Int32)" && - m.Name != "joyGetPosEx(UInt32,WindowsGamePad+JoyInfoEx*)" && - !m.FullName.Contains("WebCam") && - !m.FullName.Contains("NVorbis") && - !m.FullName.Contains("Invokes") && - !m.ParentType.IsRecord && - !m.Name.StartsWith("<") && !m.FullName.Contains("$") && - !m.IsSpecialName && - !m.IsGeneratedByCompiler && - !m.IsExplicitInterfaceImpl // This can generate unexpected method name. -select new { - m, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about *methods* whose names don't start -// with an Upper character. It is a common practice in the .NET -// world to use **Pascal Casing Style** to name methods. -// -// **Pascal Casing Style** : The first letter in the identifier -// and the first letter of each subsequent concatenated word -// are capitalized. For example: *ComputeSize* -// - -// -// *Pascal Case* the names of matched methods. -//]]> - Do not name enum values 'Reserved' -// ND2008:DoNotNameEnumValuesReserved - -warnif count > 0 -from f in Application.Fields where - f.IsEnumValue && - f.NameLike (@"Reserved") -select new { - f, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule assumes that an enumeration member -// with a name that contains **"Reserved"** -// is not currently used but is a placeholder to -// be renamed or removed in a future version. -// Renaming or removing a member is a breaking -// change. You should not expect users to ignore -// a member just because its name contains -// **"Reserved"** nor can you rely on users to read or -// abide by documentation. Furthermore, because -// reserved members appear in object browsers -// and smart integrated development environments, -// they can cause confusion as to which members -// are actually being used. -// -// Instead of using a reserved member, add a -// new member to the enumeration in the future -// version. -// -// In most cases, the addition of the new -// member is not a breaking change, as long as the -// addition does not cause the values of the -// original members to change. -// - -// -// To fix a violation of this rule, remove or -// rename the member. -//]]> - Avoid types with name too long -// ND2009:AvoidTypesWithNameTooLong - -warnif count > 0 -from t in JustMyCode.Types -where !t.IsGeneratedByCompiler && - !t.FullName.Contains("ComponentValidator") && - !t.FullName.Contains("OpenALSoundFile") && - !t.FullName.Contains("OpenALSoundDevice") - -where t.SimpleName.Length > -(t.SimpleName == "RenderableComponentIsAddedAutomaticallyWhenAddingAnyRenderData" || - t.SimpleName == "SharedComponentMustHaveAPublicStaticSharedFieldButNotSharedWithData" || - t.SimpleName == "SharedComponentsShouldNotContainAnyDataUseSharedComponentWithDataInstead" || - t.SimpleName == "NodeCombinationOrderMustBeUniqueItWasDefinedDifferentlyBefore" || - t.SimpleName == "SceneOrNamespaceDoesNotExistCheckIfAssemblyExistsAndIsNotStripped" || - t.SimpleName == "NoAuthenticationProviderSpecifiedAddProjectWithAuthenticationSupport" || - t.SimpleName == "NoAchievementProviderSpecifiedAddProjectWithAchievementSupport" || - t.SimpleName == "CannotBecomeParentToChildEntityThatAlreadyHasThisEntityAsAParent" || - t.SimpleName == "IncreaseRenderDataPropertyFromTriggerOrValueMessage" ? 70 : 60) -orderby t.SimpleName.Length descending -select new { - t, - t.SimpleName, - NameLength = t.SimpleName.Length, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Types with a name too long tend to decrease code readability. -// This might also be an indication that a type is doing too much. -// -// This rule matches types with names with more than 40 characters. -// - -// -// To fix a violation of this rule, rename the type with a shortest name -// or eventually split the type in several more fine-grained types. -//]]> - Avoid methods with name too long -// ND2010:AvoidMethodsWithNameTooLong - -warnif count > 0 - -// First get test method for which we allow long names -let testAttr = ThirdParty.Types.WithNameIn("FactAttribute", "TestAttribute", "TestCaseAttribute") -let testMethods = Methods.TaggedWithAnyAttributes(testAttr).ToHashSetEx() - -from m in JustMyCode.Methods where - - // Explicit Interface Implementation methods are - // discarded because their names are prefixed - // with the interface name. - !m.Name.Contains("_UseChangeableListWithWarningsForComponentsAndChildren") && - !m.FullName.Contains("AppRunner") && - !m.FullName.Contains("ComponentValidator") && - - !m.IsExplicitInterfaceImpl && - !m.IsGeneratedByCompiler && - ((!m.IsSpecialName && m.SimpleName.Length > (!m.IsPublic || m.FullName.Contains("Tests") ? 70 : 50)) || - // Property getter/setter are prefixed with "get_" "set_" of length 4. - ( m.IsSpecialName && m.SimpleName.Length - 4 > 50)) && - - // Don't match test methods - !testMethods.Contains(m) && - !m.SimpleName.Contains("Test") && - !m.ParentType.SimpleName.Contains("Test") && - !m.ParentNamespace.Name.Contains("Test") - -orderby m.SimpleName.Length descending - -select new { - m, - m.SimpleName, - NameLength = m.SimpleName.Length - (m.IsSpecialName ? 4 : 0), - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Methods with a name too long tend to decrease code readability. -// This might also be an indication that a method is doing too much. -// -// This rule matches methods with names with more than 40 characters. -// -// However it is considered as a good practice to name unit tests -// in such a way with a very expressive name, hence this rule doens't match -// methods tagged with *FactAttribute*, *TestAttribute* and *TestCaseAttribute*. -// - -// -// To fix a violation of this rule, rename the method with a shortest name -// that equally conveys the behavior of the method. -// Or eventually split the method into several smaller methods. -//]]> - Avoid fields with name too long -// ND2011:AvoidFieldsWithNameTooLong - -warnif count > 0 from f in JustMyCode.Fields where - !f.IsGeneratedByCompiler && - !f.FullName.Contains("UnitTestingExtensions") && - f.Name.Length > 45 -orderby f.Name descending -select new { - f, - NameLength = f.Name.Length, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// Fields with a name too long tend to decrease code readability. -// -// This rule matches fields with names with more than 40 characters. -// - -// -// To fix a violation of this rule, rename the field with a shortest name -// that equally conveys the same information. -//]]> - Avoid having different types with same name -// ND2012:AvoidHavingDifferentTypesWithSameName - -warnif count > 0 - -// Special type names for which multiple types with such name are allowed. -let nonReportedTypeName = new [] { - "Program", "NamespaceDoc", "Initial", "Startup", "SwaggerConfig", "Constants", "Scene", "Image", "All", "PixelFormat", "HttpStatusCode", "FilterInfo" } - -// Special type name suffixes for which multiple types with such suffix are allowed. -let nonReportedTypeNameSuffix = new [] { - "Service", "Model", "Controller", "Page", "Pages" } - -// This rule matches also collisions between -// application and third-party types sharing a same name. -let groups = JustMyCode.Types.Union(ThirdParty.Types) - // Discard nested types, whose name is - // prefixed with the parent type name. - .Where(t => !t.IsNested && - !nonReportedTypeName.Contains(t.Name) && - !t.Name.EndsWithAny(nonReportedTypeNameSuffix)) - - // Group types by name. - .GroupBy(t => t.Name) - -from @group in groups - where @group.Count() > 1 && - - // If the namespace is completely different (e.g. different sample games this rule makes no sense at all, all games can have a Menu, Level, etc.) - @group.First().ParentNamespace.Name.Split('.')[0] != - @group.First().ParentNamespace.Name.Split('.')[0] && - @group.First().Name != "Component" && - @group.First().Name != "Color" && - @group.First().Name != "Size" && - @group.First().Name != "Image" && - @group.First().Name != "Content" && - @group.First().Name != "SystemInformation" && - @group.First().Name != "Scene" && - @group.First().Name != "Device" && - @group.First().Name != "Options" && - @group.First().Name != "Program" && - @group.First().Name != "ProgramTests" - - // Let's see if types with the same name are declared - // in different namespaces. - // (t.FullName is {namespaceName}.{typeName} ) - let groupsFullName = @group.GroupBy(t => t.FullName) - where groupsFullName.Count() > 1 - - // If several types with same name are declared in different namespaces - // eliminate the case where all types are declared in third-party assemblies. - let types= groupsFullName.SelectMany(g => g) - where types.Any(t => !t.IsThirdParty) - // Uncomment this line, to only gets naming collision involving - // both application and third-party types. - // && types.Any(t => t.IsThirdParty) - -orderby types.Count() descending - -select new { - // Order types by parent namespace and assembly name,this way we always get the same type across sessions. - // Without this astute, the same issue would be seen as added/removed when the first type choosen in the group - // was not always the same, a situation that actually happens. - // Also make sure that the chosen type is not a third-party one. - t = types.OrderBy(t => (t.IsThirdParty ? "1" : "0") + t.ParentNamespace.Name + t.ParentAssembly.Name).First(), - - // In the 'types' column, make sure to group matched types - // by parent assemblies and parent namespaces, to get a result - // more readable. - types, - - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns about multiple types with same name, -// that are defined in different *application* or -// *third-party* namespaces or assemblies. -// -// Such practice create confusion and also naming collision -// in source files that use different types with same name. -// - -// -// To fix a violation of this rule, rename concerned types. -//]]> - Avoid prefixing type name with parent namespace name -// ND2013:AvoidPrefixingTypeNameWithParentNamespaceName - -warnif count > 0 - -from n in JustMyCode.Namespaces -where n.Name.Length > 0 - -from t in n.ChildTypes -where - JustMyCode.Contains(t) && // Don't warn about generated code - !t.IsGeneratedByCompiler && - !t.IsNested && - !t.Name.StartsWith("Xml") && - !t.Name.StartsWith("Json") && - !t.FullName.Contains("SettingsStore") && - !t.Name.StartsWith("ContentFile") && - !t.Name.StartsWith("BinaryData") && - !t.Name.StartsWith("Input") && - !t.Name.StartsWith("Multimedia") && - !t.Name.StartsWith("Unified") && - !t.Name.StartsWith("Analytics") && - !t.Name.StartsWith("Authentication") && - !t.Name.Contains("GraphicsContext") && - !t.Name.StartsWith("Tcp") && - !t.Name.Contains("ContentPlatform") && - !t.Name.Contains("VerticesMode") && - !t.Name.Contains("NetworkingResolver") && - !t.Name.Contains("WebClientCreator") && - !t.Name.StartsWith("TachyonServer") && - t.Name.IndexOf(n.SimpleName) == 0 && - - // The type name is equal to namespace name or the type name contains another - // word that starts with an upper-case letter after the namespace name. - // This way we avoid matching false-positive where namespace name is "Stat" and type name is "Statistic". - (t.Name.Length == n.SimpleName.Length || char.IsUpper(t.Name[n.SimpleName.Length])) -select new { - t, - namespaceName = n.SimpleName, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about situations where the parent namespace name -// is used as the prefix of a contained type. -// -// For example a type named "RuntimeEnvironment" -// declared in a namespace named "Foo.Runtime" -// should be named "Environment". -// -// Such situation creates naming redundancy with no readability gain. -// - -// -// To fix a violation of this rule, remove the prefix from the type name. -//]]> - Avoid naming types and namespaces with the same identifier -// ND2014:AvoidNamingTypesAndNamespacesWithTheSameIdentifier - -warnif count > 0 -let hashsetShortNames = Namespaces.Where(n => n.Name.Length > 0).Select(n => n.SimpleName).ToHashSetEx() - -from t in JustMyCode.Types -where hashsetShortNames.Contains(t.Name) && - t.Name != "Drawing" && - t.Name != "Text" && - t.Name != "Server" && -// Do not report if the whole namespace is different (e.g. Menu, Level, etc. in different samples) - Namespaces.All(n => n.SimpleName == t.Name && - n.Name.Split('.')[0] == t.ParentNamespace.Name.Split('.')[0]) -select new { - t, - namespaces = Namespaces.Where(n => n.SimpleName == t.Name), - Debt = 12.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns when a type and a namespace have the same name. -// -// For example when a type is named *Environment* -// and a namespace is named *Foo.Environment*. -// -// Such situation provokes tedious compiler resolution collision, -// and makes the code less readable because concepts are not -// concisely identified. -// - -// -// To fix a violation of this rule, renamed the concerned type or namespace. -//]]> - Don't call your method Dispose -// ND2015:DontCallYourMethodDispose - -warnif count > 0 - -// Activate this rule only when IDisposable is resolved in third-party code -// else this rule might produce false positives. -let thirdPartyDisposable = ThirdParty.Types.WithFullName("System.IDisposable").FirstOrDefault() -where thirdPartyDisposable != null - -from m in JustMyCode.Methods.WithSimpleName("Dispose") -where !m.ParentType.Implement("System.IDisposable".AllowNoMatch()) - && m.OverriddensBase.Count() == 0 // Can't change the name of an override - && !m.IsNewSlot // new slot also means override of an intreface method -select new { - m, - Debt = 15.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// In .NET programming, the identifier *Dispose* should be kept -// only for implementations of *System.IDisposable*. -// -// This rule warns when a method is named *Dispose()*, -// but the parent type doesn't implement *System.IDisposable*. -// - -// -// To fix a violation of this rule, -// either make the parent type implements *System.IDisposable*, -// or rename the *Dispose()* method with another identifier like: -// *Close() Terminate() Finish() Quit() Exit() Unlock() ShutDown()*… -//]]> - Methods prefixed with 'Try' should return a boolean (or be a void method throwing exceptions) -// ND2016:MethodsPrefixedWithTryShouldReturnABoolean - -warnif count > 0 -from m in Application.Methods where - m.SimpleNameLike("^Try") && - !m.Name.StartsWith("TryParse") && - m.Parent.Name != "XmlFile" && - m.Parent.Name != "XmlSnippet" && - m.Parent.Name != "InternetHttpClient" && - !m.FullName.Contains("NVorbis") && - !m.FullName.Contains("VideoInputs") && - m.ReturnType != null && - // Exclude nullable return types is sadly still not working with NDepend - m.ReturnType.BaseClass != null && - m.ReturnType.BaseClass.Name != "Expression" && - m.ReturnType.FullName != "System.Boolean" && - m.ReturnType.FullName != "System.Void" && - m.ReturnType.FullName != "System.IO.Stream" && - m.ReturnType.FullName != "System.Threading.Tasks.Task" && - m.ReturnType.FullName != "System.Action" && - m.ReturnType.FullName != "System.Object" && - m.ReturnType.FullName != "System.Collections.Generic.ISet" -select new { - m, - m.ReturnType, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// When a method has a name prefixed with **Try**, it is expected that -// it returns a *boolean*, that reflects the method execution status, -// *success* or *failure*. -// -// Such method usually returns a result through an *out parameter*. -// For example: *System.Int32.TryParse(int,out string):bool* -// - -// -// To fix a violation of this rule, -// Rename the method, or transform it into an operation that can fail. -//]]> - Properties and fields that represent a collection of items should be named Items. -// ND2017:PropertiesAndFieldsThatRepresentACollectionOfItemsShouldBeNamedItems - -warnif count > 0 - -let collectionTypes = Types.Where(t => - t.Implement("System.Collections.Generic.IEnumerable".AllowNoMatch()) && - t.IsGeneric && - t.FullName != "System.Collections.Generic.IDictionary" && - !t.Implement("System.Collections.Generic.IDictionary".AllowNoMatch()) && - t.FullName != "System.Collections.Generic.IReadOnlyDictionary" && - !t.Implement("System.Collections.Generic.IReadOnlyDictionary".AllowNoMatch())).ToHashSetEx() - -let properties = from m in JustMyCode.Methods -where (m.IsPropertyGetter || m.IsPropertySetter) && - collectionTypes.Contains(m.ReturnType) -select new { Member = (IMember)m, Type = m.ReturnType } - -let fields = from f in JustMyCode.Fields -where collectionTypes.Contains(f.FieldType) -select new { Member = (IMember)f, Type = f.FieldType } - -from pair in properties.Concat(fields) -let identifier = pair.Member.SimpleName -where !identifier.EndsWith("s") && - !identifier.Contains('<') // Remove potential generated fields like backing fields, and also potential generated properties accessors - -let identifierRefined = identifier.Replace("get_", "").Replace("set_", "") -let words = identifierRefined.GetWords() -where !words.Any(word => - word.EndsWith("s") || // Don't warn if any word ends with an s - word == "Empty") // Don't warn if any word in the identifier is Empty - -select new { pair.Member, pair.Type } - -// -// A good practice to make the code more readable and more predictable -// is to name properties and fields typed with a collection of *items* -// with the plural form of *Items*. -// -// Depending on the domain of your application, a proper identifier could be -// *NewDirectories*, *Words*, *Values*, *UpdatedDates*. -// -// Also this rule doesn't warn when any word in the identifier ends with an *s*. -// This way identifiers like **TasksToRun**, **KeysDisabled**, **VersionsSupported**, -// **ChildrenFilesPath**, **DatedValuesDescending**, **ApplicationNodesChanged** -// or **ListOfElementsInResult** are valid and won't be seen as violations. -// -// Moreover this rule won't warn for a field or property with an identifier -// that contain the word **Empty**. -// This is a common pattern to define an immutable and empty collection instance -// shared. -// -// Before inspecting properties and fields, this rule gathers -// application and third-party collection types that might be returned -// by a property or a field. To do so this rule searches types that implement -// *IEnumerable* except: -// -// - Non generic types: Often a non generic type is not seen as a collection. -// For example *System.String* implements *IEnumerable*, but a string -// is rarely named as a collection of characters. In others words, -// we have much more strings in our program named like *FirstName* -// than named like *EndingCharacters*. -// -// - Dictionaries types: A dictionary is more than a collection of pairs, -// it is a mapping from one domain to another. A common practice is to suffix -// the name of a dictionary with *Map* *Table* or *Dictionary*, -// although often dictionaries names satify this rule with names like -// *GuidsToPersons* or *PersonsByNames*. -// - -// -// Just rename the fields and properties accordingly, -// by making plural the word in the identifier -// that describes best the *items* in the collection. -// -// For example: -// -// - **ListOfDir** can be renamed **Directories**. -// -// - **Children** can be renamed **ChildrenItems** -// -// - **QueueForCache** can be renamed **QueueOfItemsForCache** -//]]> - DDD ubiquitous language check -// ND2018:DDDUbiquitousLanguageCheck - -warnif count > 0 - -// Update to your core domain namespace(s) -let coreDomainNamespaces = Application.Namespaces.WithNameLike("TrainTrain.Domain") - -// Update your vocabulary list -let vocabulary = new [] { -"Train", "Coach", "Coaches", "Seat", "Seats", -"Reservation", "Fulfilled", "Booking", "Book", "Reserve", "Confirm" -}.ToHashSetEx() - -let technicalWords = new [] { "get", "set", "Get", "Set", "Add" }.ToHashSetEx() -let multiWordsVocabulary = vocabulary.Where(w => w.GetWords().Length >= 2) - -// Append multi-words words in vocabulary -let multiWordsWords = multiWordsVocabulary.SelectMany(w => w.GetWords()) -let vocabulary2 = vocabulary.Concat(multiWordsWords).ToHashSetEx() -let vocabulary3 = vocabulary2.Concat(technicalWords).ToHashSetEx() - -from ce in coreDomainNamespaces.ChildTypesAndMembers() -let tokens = ce.SimpleName.GetWords().Select(w => w.FirstCharToUpper()).ToArray() - -where !(ce.IsMethod && ce.AsMethod.IsConstructor) && // No vocabulary in ctor - !(ce.IsField && ce.AsField.IsGeneratedByCompiler) && // Remove compiler generated backing fields, their vocabulary is in property - !(vocabulary3.Any(vocable => tokens.Contains(vocable))) - -let wordsNotInVocabulary = tokens.Where(w => !(vocabulary2.Contains(w) && string.IsNullOrEmpty(w))).ToArray() - -select new { ce, - wordsNotInVocabulary = string.Join(", ",wordsNotInVocabulary) -} - -// -// The language used in identifiers of classes, methods and fields of the **core domain**, -// should be based on the **Domain Model**. -// This constraint is known as **ubiquitous language** in **Domain Driven Design (DDD)** -// and it reflects the need to be rigorous with naming, -// since software doesn't cope well with ambiguity. -// -// This rule is disabled per default -// because its source code needs to be customized to work, -// both with the **core domain** namespace name -// (that contains classes and types to be checked), -// and with the list of domain language terms. -// -// If a term needs to be used both with singular and plural forms, -// both forms need to be mentioned, -// like **Seat** and **Seats** for example. -// Notice that this default rule is related with the other default rule -// *Properties and fields that represent a collection of items should be named Items* -// defined in the *Naming Convention* group. -// -// This rule implementation relies on the NDepend API -// **ExtensionMethodsString.GetWords(this string identifier)** -// extension method that extracts terms -// from classes, methods and fields identifiers in a smart way. -// - -// -// For each violation, this rule provides the list of **words not in vocabulary**. -// -// To fix a violation of this rule, either rename the concerned code element -// or update the domain language terms list defined in this rule source code, -// with the missing term(s). -//]]> - Avoid fields with same name in class hierarchy -// ND2019:AvoidFieldsWithSameNameInClassHierarchy - -warnif count > 0 - -// Optimization: only consider fields of derived classes -let derivedClasses = Application.Types.Where(t => - t.IsClass && - !t.IsStatic && -!t.FullName.StartsWith("NVorbis") && - t.DepthOfInheritance > 1 && - t.BaseClasses.ChildFields().Any()) - -from f in derivedClasses.ChildFields() -where JustMyCode.Contains(f) // Make sure it is fixable by targeting only JustMyCode fields -let fieldsOfBaseClassesWithSameName = - f.ParentType - .BaseClasses - .ChildFields() - .Where(cf => cf.Name == f.Name && JustMyCode.Contains(cf)) -where fieldsOfBaseClassesWithSameName.Any() - -select new { - f, - fieldsOfBaseClassesWithSameName, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule matches a field whose name is already used by -// another field in the base class hierarchy. -// -// The C# and VB.NET compilers allow this situation even -// if the base class field is visible to derived classes -// with a visibility different than *private*. -// -// However this situation is certainly a sign of -// an erroneous attempt to redefine a field in a derived class. -// - -// -// Check if the field in the derived class is indeed a redefinition -// of the base class field. Check also that both fields types -// corresponds. If fields are static, double check that only a single -// instance of the referenced object is needed. If all checks are positive -// delete the derived class field and make sure that the base class -// field is visible to derived classes with the *protected* visibility. -// -// If no, rename the field in the derived class and be very careful -// in renaming all usages of this field, they might be related -// with the base class field. -//]]> - Avoid various capitalizations for method name -// ND2020:AvoidVariousCapitalizationsForMethodName - -warnif count > 0 - -let lookup = JustMyCode.Methods.ToLookup(m => m.SimpleName.ToLower()) -from grouping in lookup -where grouping.Count() > 1 -let name = grouping.First().SimpleName -let method = grouping.FirstOrDefault(m => m.SimpleName != name && m.SimpleName == "ServerConnection") -where method != null -let nbCapitalizations = grouping.ToLookup(m => m.SimpleName).Count - -// Increase the severity if various capitalizations are found in same class -let differentCapitalizationInSameType = - grouping.ToLookup(m => m.ParentType) - .Any(groupingPerType => groupingPerType.ToLookup(m => m.SimpleName).Count > 1) - -select new { - method, - methods = grouping.ToArray(), - nbCapitalizations, - Debt = (nbCapitalizations * 6).ToMinutes().ToDebt(), - Severity = differentCapitalizationInSameType ? Severity.Critical : Severity.Medium -} - -// -// This rule matches application methods whose names differ -// only in capitalization. -// -// With this rule you'll discover various names like -// *ProcessOrderId* and *ProcessOrderID*. It is important to -// choose a single capitalization used accross the whole application. -// -// Matched methods are not necessarily declared in the same type. -// However if various capitalizations of a method name are found -// within the same type, the issue severity goes from *Medium* -// to *Critical*. Indeed, this situation is not just a matter -// of convention, it is error-prone. It forces type's clients -// to investigate which version of the method to call. -// - -// -// Choose a single capitalization for the method name -// used accross the whole application. -// -// Or alternatively make the distinction clear by having -// different method names that don't only differ by -// capitalization. -// -// The technical-debt for each issue, the estimated cost -// to fix an issue, is proportional to the number -// of capitalizations found (2 minimum). -//]]> - - - Avoid referencing source file out of Visual Studio project directory -// ND2100:AvoidReferencingSourceFileOutOfVisualStudioProjectDirectory - -warnif count > 0 - -from a in Application.Assemblies -where a.VisualStudioProjectFilePath != null -let vsProjDirPathLower = a.VisualStudioProjectFilePath.ParentDirectoryPath.ToString().ToLower() - -from t in a.ChildTypes -where JustMyCode.Contains(t) && t.SourceFileDeclAvailable - -from decl in t.SourceDecls -let sourceFilePathLower = decl.SourceFile.FilePath.ToString().ToLower() -where sourceFilePathLower.IndexOf(vsProjDirPathLower) != 0 -select new { - t, - sourceFilePathLower, - projectFilePath = a.VisualStudioProjectFilePath.ToString(), - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// A source file located outside of the VS project directory can be added through: -// *> Add > Existing Items… > Add As Link* -// -// Doing so can be used to share types definitions across several assemblies. -// This provokes type duplication at binary level. -// Hence maintainability is degraded and subtle versioning bug can appear. -// -// This rule matches types whose source files are not declared under the -// directory that contains the related Visual Studio project file, or under -// any sub-directory of this directory. -// -// This practice can be tolerated for certain types shared across executable assemblies. -// Such type can be responsible for startup related concerns, -// such as registering custom assembly resolving handlers or -// checking the .NET Framework version before loading any custom library. -// - -// -// To fix a violation of this rule, prefer referencing from a VS project -// only source files defined in sub-directories of the VS project file location. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - Avoid duplicating a type definition across assemblies -// ND2101:AvoidDuplicatingATypeDefinitionAcrossAssemblies - -warnif count > 0 - -let groups = Application.Types - .Where(t => !t.IsGeneratedByCompiler && - // Types created by the test infrastructure - t.FullName != "AutoGeneratedProgram") - .GroupBy(t => t.FullName) -from @group in groups -where @group.Count() > 1 - -// Tricky: This rule is executed on both current snapshot and baseline snapshot (if any). -// Taking t as @group.First() could return any type in the group, and potentially -// it can return different types for current and baseline snapshot. -// As a result issues wouldn't corresponds (since types are different) and the user -// would see issues of this rule as a couple of issues added and removed. -// Ordering types by parent assembly name and then taking the first type, discards this risk. -let types = @group.OrderBy(t => t.ParentAssembly.Name) - -select new { - t = types.First(), - // In the 'types' column, make sure to group matched types by parent assemblies. - typesDefs = types.ToArray(), - Debt = 15.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// A source file located outside of the VS project directory can be added through: -// *> Add > Existing Items… > Add As Link* -// -// This rule warns about using this feature to share code across several assemblies. -// This provokes type duplication at binary level. -// Hence maintainability is degraded and subtle versioning bug can appear. -// -// Each match represents a type duplicated. Unfolding the types in the column -// **typesDefs** will show the multiple definitions across multiple assemblies. -// -// This practice can be tolerated for certain types shared across executable assemblies. -// Such type can be responsible for startup related concerns, -// such as registering custom assembly resolving handlers or -// checking the .NET Framework version before loading any custom library. -// - -// -// To fix a violation of this rule, prefer sharing types through DLLs. -//]]> - Avoid defining multiple types in a source file -// ND2102:AvoidDefiningMultipleTypesInASourceFile - -warnif count > 0 - -// Build a lookup indexed by source files, values being a sequence of types defined in the source file. -let lookup = JustMyCode.Types - // When a source file is referenced by several assemblies, - // type(s) contained in the source file are seen as distinct types, both by the CLR and by NDepend. - // This Distinct clause based on type full-name (type name prefixed with namespace) - // avoids matching the multiple versions of such type. - .Distinct(t => t.FullName) - .Where(t => t.SourceFileDeclAvailable && - // except enumerations, nested types and types generated by compilers! - !t.IsEnumeration && - !t.IsNested && - !t.ParentNamespace.Name.EndsWith("Internals") && - !t.FullName.Contains("Strict.ObjectDetection.InfraredDotDetector") && - !t.IsGeneratedByCompiler) - // We use multi-key, since a type can be declared in multiple source files. - .ToMultiKeyLookup(t => t.SourceDecls.Select(d => d.SourceFile)) - -from @group in lookup where @group.Count() > 1 - // Exclude RenderableNode, Lerp, etc. which is in Lerp.cs file - where !@group.Last().Name.Contains(@group.First().Name.Replace(">", "")) && - !@group.First().Name.Contains(@group.Last().Name) && - !@group.First().Name.Contains("RenderableNode") - let sourceFile = @group.Key - - // CQLinq doesn't let indexing result with sourceFile - // so we choose a typeIndex in types, - // preferably the type that has the file name. - let typeWithSourceFileName = @group.FirstOrDefault(t => t.SimpleName == sourceFile.FileNameWithoutExtension) - let typeIndex = typeWithSourceFileName ?? @group.First() - -select new { - typeIndex, - TypesInSourceFile = @group as IEnumerable, - SourceFilePathString = sourceFile.FilePathString, - Debt = 3.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// Defining multiple types in a single source file decreases code readability, -// because developers are used to see all types in a namespace, -// when expanding a folder in the *Visual Studio Solution Explorer*. -// Also doing so, leads to source files with too many lines. -// -// Each match of this rule is a source file that contains several types -// definitions, indexed by one of those types, preferably the one with -// the same name than the source file name without file extension, if any. -// - -// -// To fix a violation of this rule, create a source file for each type. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - Namespace name should correspond to file location -// ND2103:NamespaceNameShouldCorrespondToFileLocation - -warnif count > 0 - -from a in Application.Assemblies -let assemblyName = a.Name - -from n in a.ChildNamespaces -let namespaceName = n.Name - -// Build the dirShouldContain string -// and then we check which source file path contains dirShouldContain or not -let dirShouldContain = ( - // namespaceName starts with assembly name => gets only components after the assembly name - namespaceName.StartsWith(assemblyName) - ? namespaceName.Substring(assemblyName.Length) - - // namespaceName contains assembly name => gets only components after the assembly name - : namespaceName.Contains("." + assemblyName) - ? namespaceName.Substring(n.Name.IndexOf("." + assemblyName)) - - // namespaceName has more than one part => gets only components after the first part - : namespaceName.Contains(".") - ? namespaceName.Substring(n.Name.IndexOf(".")) - : namespaceName) - - // Replace dots by spaces in namespace name - .Replace('.', ' ') - -where dirShouldContain.Length > 0 - -// Look at source file decl of JustMyCode type's declared in the namespace 'n' -from t in n.ChildTypes -where JustMyCode.Contains(t) && t.SourceFileDeclAvailable && !t.FullName.Contains("Frameworks") - -let sourceDeclConcerned = (from decl in t.SourceDecls - let sourceFilePath = decl.SourceFile.FilePath.ToString() - // Replace dots and path separators by spaces in source files names - where !sourceFilePath.Replace('.',' ').Replace('\\',' ').Contains(dirShouldContain) - select sourceFilePath).ToArray() -where sourceDeclConcerned.Length > 0 - -let justACaseSensitiveIssue = sourceDeclConcerned[0].ToLower().Replace('.',' ').Replace('\\',' ').Contains(dirShouldContain.ToLower()) - -select new { - t, - dirShouldContain, - sourceFilePath = sourceDeclConcerned[0], - nbSourceDeclConcerned = sourceDeclConcerned.Length, - justACaseSensitiveIssue , - Debt = (justACaseSensitiveIssue ? - 1f + sourceDeclConcerned.Length/2f : - 2f + sourceDeclConcerned.Length).ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// For a solid code structure and organization, -// do mirror the namespaces hierarchy and the directories hierarchy containing source files. -// -// Doing so is a widely accepted convention, and not respecting this convention -// will lead to less maintainable and less browsable source code. -// -// This rule matches all types in such source file, whose location doesn't correspond -// to the type parent namespace. If a source file contains several such types (that -// are not necessarily in the same namespace) each type will result in a violation. -// -// If a type is declared in several such source files, the value for the column -// *nbSourceDeclConcerned* in the result, is greater than 1. -// The technical-debt per issue is proportional to *nbSourceDeclConcerned*. -// -// Notice that namespaces and directories names comparison is **case-sensitive**. -// A boolean *justACaseSensitiveIssue* indicates if it is just a case-sensitive issue, -// in which case the technical-debt is divided by two. -// - -// -// To fix a violation of this rule, make sure that the type parent namespace and -// the directory sub-paths that contains the type source file, are mirrored. -// -// Make sure to first check the boolean *justACaseSensitiveIssue*, in which case -// the issue is easier to fix. -// -]]> - Types with source files stored in the same directory, should be declared in the same namespace -// ND2104:TypesWithSourceFilesStoredInTheSameDirectoryShouldBeDeclaredInTheSameNamespace - -warnif count > 0 - -// Group JustMyCode types in a lookup -// where groups are keyed with directories that contain the types' source file(s). -// Note that a type can be contained in several groups -// if it is declared in several source files stored in different directories. -let lookup = JustMyCode.Types.Where(t => t.SourceFileDeclAvailable) - .ToMultiKeyLookup( - t => t.SourceDecls.Select( - decl => decl.SourceFile.FilePath.ParentDirectoryPath).Distinct() - ) - -from groupOfTypes in lookup -let parentNamespaces = groupOfTypes.ParentNamespaces() - -// Select group of types (with source files stored in the same directory) … -// … but contained in several namespaces -where parentNamespaces.Count() > 1 - -// mainNamespaces is the namespace that contains many types -// declared in the directory groupOfTypes .key -let mainNamespace = groupOfTypes - .ToLookup(t => t.ParentNamespace) - .OrderByDescending(g => g.Count()).First().Key - -// Select types with source files stored in the same directory, -// but contained in namespaces different than mainNamespace. -let typesOutOfMainNamespace = groupOfTypes - .Where(t => t.ParentNamespace != mainNamespace && - t.ParentAssembly == mainNamespace.ParentAssembly) - - // Filter types declared on several source files that contain generated methods - // because typically such type contains one or several partial definitions generated. - // These partially generated types would be false positive for the present rule. - .Where(t => t.SourceDecls.Count() == 1 || - t.Methods.Count(m => JustMyCode.Contains(m)) == 0) -where typesOutOfMainNamespace.Count() > 0 - -let typesInMainNamespace = groupOfTypes.Where(t => t.ParentNamespace == mainNamespace) - -select new { - mainNamespace, - typesOutOfMainNamespace, - typesInMainNamespace, - Debt = (2+5*typesOutOfMainNamespace.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// For a solid code structure and organization, do mirror the namespaces -// hierarchy and the directories hierarchy containing source files. -// -// Doing so is a widely accepted convention, and not respecting this convention -// will lead to less maintainable and less browsable code. -// -// Respecting this convention means that types with source files stored in the same directory, -// should be declared in the same namespace. -// -// For each directory that contains several source files, where most types are declared -// in a namespace (what we call the **main namespace**) and a few types are declared -// out of the *main namespace*, this code rule matches: -// -// • The *main namespace* -// -// • **typesOutOfMainNamespace**: Types declared in source files in the *main namespace*'s directory -// but that are not in the *main namespace*. -// -// • *typesInMainNamespace*: And for informational purposes, types declared in source files in the -// *main namespace*'s directory, and that are in the *main namespace*. -// - -// -// Violations of this rule are types in the *typesOutOfMainNamespace* column. -// Typically such type … -// -// • … is contained in the wrong namespace but its source file is stored in the right directory. -// In such situation the type should be contained in *main namespace*. -// -// • … is contained in the right namespace but its source file is stored in the wrong directory -// In such situation the source file of the type must be moved to the proper parent namespace directory. -// -// • … is declared in multiple source files, stored in different directories. -// In such situation it is preferable that all source files are stored in a single directory. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 2 minutes plus 5 minutes per type in *typesOutOfMainNamespace*. -//]]> - Types declared in the same namespace, should have their source files stored in the same directory -// ND2105:TypesDeclaredInTheSameNamespaceShouldHaveTheirSourceFilesStoredInTheSameDirectory - -warnif count > 0 -from @namespace in Application.Namespaces - -// Group types of @namespace in a lookup -// where groups are keyed with directories that contain the types' source file(s). -// Note that a type can be contained in several groups -// if it is declared in several source files stored in different directories. -let lookup = @namespace.ChildTypes.Where( - t => t.SourceFileDeclAvailable && - JustMyCode.Contains(t) && - // Don't match ASP.NET application types declared in Global.asax file - // that typically are in the root directory of the VS project. - t.SourceDecls.First().SourceFile.FileNameWithoutExtension.ToLower() != "global.asax") - .ToMultiKeyLookup( - t => t.SourceDecls.Select( - decl => decl.SourceFile.FilePath.ParentDirectoryPath).Distinct() - ) - -// Are types of @namespaces declared in more than one directory? -where lookup.Count > 1 - -// Infer the main folder, preferably the one that has the same name as the namespace. -let dirs = lookup.Select(types => types.Key) -let mainDirNullable = dirs.Where(d => d.DirectoryName == @namespace.SimpleName).FirstOrDefault() -let mainDir = mainDirNullable ?? dirs.First() - -// Types declared out of mainDir, are types in group of types declared in a directory different than mainDir! -let typesDeclaredOutOfMainDir = - lookup.Where(types => types.Key != mainDir) - .SelectMany(types => types) - - // Filter types declared on several source files that contain generated methods - // because typically such type contains one or several partial definitions generated. - // These partially generated types would be false positive for the present rule. - .Where(t => t.SourceDecls.Count() == 1 || - t.Methods.Count(m => JustMyCode.Contains(m)) == 0) - -where typesDeclaredOutOfMainDir.Count() > 0 - -let typesDeclaredInMainDir = - lookup.Where(types => types.Key == mainDir) - .SelectMany(types => types) - -select new { - @namespace, - typesDeclaredOutOfMainDir, - mainDir = mainDir.ToString(), - typesDeclaredInMainDir, - Debt = (2+5*typesDeclaredOutOfMainDir.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// For a solid code structure and organization, -// do mirror the namespaces hierarchy and the directories hierarchy containing source files. -// -// Doing so is a widely accepted convention, and not respecting this convention -// will lead to less maintainable and less browsable code. -// -// Respecting this convention means that types declared in the same namespace, -// should have their source files stored in the same directory. -// -// For each namespace that contains types whose source files -// are declared in several directories, infer the **main directory**, -// the directory that naturally hosts source files of types, -// preferably the directory whose name corresponds with the namespace -// name. In this context, this code rule matches: -// -// • The namespace -// -// • **typesDeclaredOutOfMainDir**: types in the namespace whose source files -// are stored out of the *main directory*. -// -// • The *main directory* -// -// • *typesDeclaredInMainDir*: for informational purposes, types declared -// in the namespace, whose source files are stored in the *main directory*. -// - -// -// Violations of this rule are types in the **typesDeclaredOutOfMainDir** column. -// Typically such type… -// -// • … is contained in the wrong namespace but its source file is stored in the right directory. -// In such situation the type should be contained in the namespace corresponding to -// the parent directory. -// -// • … is contained in the right namespace but its source file is stored in the wrong directory. -// In such situation the source file of the type must be moved to the *main directory*. -// -// • … is declared in multiple source files, stored in different directories. -// In such situation it is preferable that all source files are stored in a single directory. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 2 minutes plus 5 minutes per type in *typesDeclaredOutOfMainDir*. -//]]> - - - - Mark ISerializable types with SerializableAttribute -// ND2200:MarkISerializableTypesWithSerializableAttribute - -warnif count > 0 - -from t in Application.Types where - t.IsPublic && - !t.IsDelegate && - !t.IsExceptionClass && // Don't match exceptions, since the Exception class - // implements ISerializable, this would generate - // too many false positives. - t.Implement ("System.Runtime.Serialization.ISerializable".AllowNoMatch()) && - !t.HasAttribute ("System.SerializableAttribute".AllowNoMatch()) - -select new { - t, - t.NbLinesOfCode, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// To be recognized by the CLR as serializable, -// types must be marked with the *SerializableAttribute* -// attribute even if the type uses a custom -// serialization routine through implementation of -// the *ISerializable* interface. -// -// This rule matches types that implement *ISerializable* and -// that are not tagged with *SerializableAttribute*. -// - -// -// To fix a violation of this rule, tag the matched type -// with *SerializableAttribute* . -//]]> - Mark assemblies with CLSCompliant (deprecated) -// ND2201:MarkAssembliesWithCLSCompliant - -warnif count > 0 from a in Application.Assemblies where - !a.Name.Contains("Frameworks") && - !a.Name.Contains("Editor.View") && - !a.Name.Contains("Editor.TypeEditors") && - !a.AssembliesUsed.Any(reference=>reference.Name.Contains("Frameworks")) && - !a.HasAttribute ("System.CLSCompliantAttribute".AllowNoMatch()) -select new { - a, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule has been deprecated and, as a consequence, it is disabled by default. -// Feel free to re-enable it if it makes sense in your dev environment. -// -// The *Common Language Specification* (CLS) defines naming restrictions, -// data types, and rules to which assemblies must conform if they are to -// be used across programming languages. Good design dictates that all -// assemblies explicitly indicate CLS compliance with **CLSCompliantAttribute**. -// If the attribute is not present on an assembly, the assembly is not compliant. -// -// Notice that it is possible for a CLS-compliant assembly to contain types or -// type members that are not compliant. -// -// This rule matches assemblies that are not tagged with -// **System.CLSCompliantAttribute**. -// - -// -// To fix a violation of this rule, tag the assembly with *CLSCompliantAttribute*. -// -// Instead of marking the whole assembly as non-compliant, you should determine -// which type or type members are not compliant and mark these elements as such. -// If possible, you should provide a CLS-compliant alternative for non-compliant -// members so that the widest possible audience can access all the functionality -// of your assembly. -//]]> - Mark assemblies with ComVisible (deprecated) -// ND2202:MarkAssembliesWithComVisible - -warnif count > 0 from a in Application.Assemblies where - !a.HasAttribute ("System.Runtime.InteropServices.ComVisibleAttribute".AllowNoMatch()) -select new { - a, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule has been deprecated and, as a consequence, it is disabled by default. -// Feel free to re-enable it if it makes sense in your dev environment. -// -// The **ComVisibleAttribute** attribute determines how COM clients access -// managed code. Good design dictates that assemblies explicitly indicate -// COM visibility. COM visibility can be set for a whole assembly and then -// overridden for individual types and type members. If the attribute is not -// present, the contents of the assembly are visible to COM clients. -// -// This rule matches assemblies that are not tagged with -// **System.Runtime.InteropServices.ComVisibleAttribute**. -// - -// -// To fix a violation of this rule, tag the assembly with *ComVisibleAttribute*. -// -// If you do not want the assembly to be visible to COM clients, set the -// attribute value to **false**. -//]]> - Mark attributes with AttributeUsageAttribute -// ND2203:MarkAttributesWithAttributeUsageAttribute - -warnif count > 0 -from t in JustMyCode.Types where - - t.DeriveFrom ("System.Attribute".AllowNoMatch()) -&& !t.HasAttribute ("System.AttributeUsageAttribute".AllowNoMatch()) - -// AttributeUsageAttribute can be deferred to classes that derive from an abstract attribute class -&& !t.IsAbstract - -// AttributeUsageAttribute can de inherited from a base attribute class (that is not System.Attribute) -&& !t.BaseClasses.Any(c => - c.FullName != "System.Attribute" && - c.HasAttribute ("System.AttributeUsageAttribute".AllowNoMatch())) - -select new { - t, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// When you define a custom attribute, mark it by using **AttributeUsageAttribute** -// to indicate where in the source code the custom attribute can be applied. The -// meaning and intended usage of an attribute will determine its valid locations -// in code. For example, you might define an attribute that identifies the person -// who is responsible for maintaining and enhancing each type in a library, and -// that responsibility is always assigned at the type level. In this case, compilers -// should enable the attribute on classes, enumerations, and interfaces, but should -// not enable it on methods, events, or properties. Organizational policies and -// procedures would dictate whether the attribute should be enabled on assemblies. -// -// The **System.AttributeTargets** enumeration defines the targets that you can -// specify for a custom attribute. If you omit *AttributeUsageAttribute*, your -// custom attribute will be valid for all targets, as defined by the **All** value of -// *AttributeTargets* enumeration. -// -// This rule matches attribute classes that are not tagged with -// **System.AttributeUsageAttribute**. -// -// Abstract attribute classes are not matched since AttributeUsageAttribute -// can be deferred to derived classes. -// -// Attribute classes that have a base class tagged with AttributeUsageAttribute -// are not matched since in this case attribute usage is inherited. -// - -// -// To fix a violation of this rule, specify targets for the attribute by using -// *AttributeUsageAttribute* with the proper *AttributeTargets* values. -//]]> - Remove calls to GC.Collect() -// ND2204:RemoveCallsToGCCollect - -warnif count > 0 - -let gcCollectMethods = ThirdParty.Methods.WithFullNameWildcardMatch( - "System.GC.Collect(*)").ToHashSetEx() - -from m in Application.Methods.UsingAny(gcCollectMethods) -select new { - m, - gcCollectMethodCalled = m.MethodsCalled.Intersect(gcCollectMethods), - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// It is preferable to avoid calling **GC.Collect()** -// explicitly in order to avoid some performance pitfall. -// -// More in information on this here: -// http://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspx -// -// This rule matches application methods that call an -// overload of the method *GC.Collect()*. -// - -// -// Remove matched calls to *GC.Collect()*. -//]]> - Don't call GC.Collect() without calling GC.WaitForPendingFinalizers() -// ND2205:DontCallGCCollectWithoutCallingGCWaitForPendingFinalizers - -warnif count > 0 - -let gcCollectMethods = ThirdParty.Methods.WithFullNameWildcardMatch( - "System.GC.Collect(*)").ToHashSetEx() - -from m in Application.Methods.UsingAny(gcCollectMethods) where - !m.IsUsing ("System.GC.WaitForPendingFinalizers()".AllowNoMatch()) -select new { - m, - gcCollectMethodCalled = m.MethodsCalled.Intersect(gcCollectMethods), - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// It is preferable to avoid calling **GC.Collect()** -// explicitly in order to avoid some performance -// pitfall. This situation is checked through the -// default rules: *Remove calls to GC.Collect()* -// -// But if you wish to call *GC.Collect()* anyway, -// you must do it this way: -// -// GC.Collect(); -// -// GC.WaitForPendingFinalizers(); -// -// GC.Collect(); -// -// To make sure that finalizer got executed, and -// object with finalizer got cleaned properly. -// -// This rule matches application methods that call an -// overload of the method *GC.Collect()*, without calling -// *GC.WaitForPendingFinalizers()*. -// - -// -// To fix a violation of this rule, if you really -// need to call *GC.Collect()*, make sure to call -// *GC.WaitForPendingFinalizers()* properly. -//]]> - Enum Storage should be Int32 -// ND2206:EnumStorageShouldBeInt32 - -warnif count > 0 from f in JustMyCode.Fields where - f.ParentType.IsEnumeration && - f.Name == @"value__" && - !f.FullName.StartsWith("NVorbis") && - !f.ParentType.IsInternal && - f.FieldType != null && - f.FieldType.FullName != "System.Int32" && - !f.IsThirdParty -select new { - f, - f.SizeOfInst, - f.FieldType, - Debt = 7.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// An enumeration is a value type that defines a set of related named constants. -// By default, the **System.Int32** data type is used to store the constant value. -// -// Even though you can change this underlying type, it is not necessary or -// recommended for most scenarios. Note that *no significant performance gain* is -// achieved by using a data type that is smaller than *Int32*. If you cannot use -// the default data type, you should use one of the Common Language System -// (CLS)-compliant integral types, *Byte*, *Int16*, *Int32*, or *Int64* to make -// sure that all values of the enumeration can be represented in CLS-compliant -// programming languages. -// -// This rule matches enumerations whose underlying type used to store -// values is not *System.Int32*. -// - -// -// To fix a violation of this rule, unless size or compatibility issues exist, -// use *Int32*. For situations where *Int32* is not large enough to hold the values, -// use *Int64*. If backward compatibility requires a smaller data type, use -// *Byte* or *Int16*. -//]]> - Do not raise too general exception types -// ND2207:DoNotRaiseTooGeneralExceptionTypes - -warnif count > 0 - -let tooGeneralExceptionTypes = ThirdParty.Types.WithFullNameIn( - "System.Exception", - "System.ApplicationException", - "System.SystemException") - -from m in JustMyCode.Methods.ThatCreateAny(tooGeneralExceptionTypes) -// Make sure we don't match constructor of exception types -// that actually instantiate System.Exception. -where (!m.IsConstructor || - tooGeneralExceptionTypes.All(t => !m.ParentType.DeriveFrom(t))) -let exceptionsCreated = tooGeneralExceptionTypes.Where(t => m.IsUsing(t)) -select new { - m, - exceptionsCreated, - Debt = (15 + 5*exceptionsCreated.Count()).ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// The following exception types are too general -// to provide sufficient information to the user: -// -// • System.Exception -// -// • System.ApplicationException -// -// • System.SystemException -// -// If you throw such a general exception type in a library or framework, -// it forces consumers to catch all exceptions, -// including unknown exceptions that they do not know how to handle. -// -// This rule matches methods that create an instance of -// such general exception class. -// - -// -// To fix a violation of this rule, change the type of the thrown exception -// to either a more derived type that already exists in the framework, -// or create your own type that derives from *System.Exception*. -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 15 minutes per method matched, plus 5 minutes per too general -// exception types instantiated by the method. -//]]> - Do not raise reserved exception types -// ND2208:DoNotRaiseReservedExceptionTypes - -warnif count > 0 - -let reservedExceptions = ThirdParty.Types.WithFullNameIn( - "System.ExecutionEngineException", - "System.IndexOutOfRangeException", - "System.OutOfMemoryException", - "System.StackOverflowException", - "System.InvalidProgramException", - "System.AccessViolationException", - "System.CannotUnloadAppDomainException", - "System.BadImageFormatException", - "System.DataMisalignedException") - -from m in Application.Methods.WithFullNameNotIn( -"JsonNode.FindJsonArrayElement(Int32)").ThatCreateAny(reservedExceptions) -let reservedExceptionsCreated = reservedExceptions.Where(t => m.IsUsing(t)) -select new { - m, - reservedExceptionsCreated, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// The following exception types are reserved -// and should be thrown only by the Common Language Runtime: -// -// • System.ExecutionEngineException -// -// • System.IndexOutOfRangeException -// -// • System.NullReferenceException -// -// • System.OutOfMemoryException -// -// • System.StackOverflowException -// -// • System.InvalidProgramException -// -// • System.AccessViolationException -// -// • System.CannotUnloadAppDomainException -// -// • System.BadImageFormatException -// -// • System.DataMisalignedException -// -// Do not throw an exception of such reserved type. -// -// This rule matches methods that create an instance of -// such reserved exception class. -// - -// -// To fix a violation of this rule, change the type of the -// thrown exception to a specific type that is not one of -// the reserved types. -// -// Concerning the particular case of a method throwing -// *System.NullReferenceException*, often the fix will be either -// to throw instead *System.ArgumentNullException*, either to -// use a contract (through MS Code Contracts API or *Debug.Assert()*) -// to signify that a null reference at that point can only be -// the consequence of a bug. -// -// More generally the idea of using a contract instead of throwing -// an exception in case of *corrupted state / bug consequence* detected -// is a powerful idea. It replaces a behavior (throwing exception) -// with a declarative assertion that basically means: at that point a bug -// somehow provoqued the detected corrupted state and continuing -// any processing from now is potentially harmful. The process should be -// shutdown and the circonstances of the failure should be reported -// as a bug to the product team. -//]]> - Uri fields should be of type System.Uri -// ND2209:UriFieldsShouldBeOfTypeSystemUri - -warnif count > 0 from f in Application.Fields where - (f.NameLike (@"Uri$") || - f.NameLike (@"Url$")) && - f.FieldType != null && - f.FieldType.FullName != "System.Uri" -select new { - f, - f.FieldType, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// A field with the name ending with *'Uri'* or *'Url'* is deemed -// to represent a *Uniform Resource Identifier or Locator*. -// Such field should be of type **System.Uri**. -// -// This rule matches fields with the name ending with *'Uri'* or -// *'Url'* that are not typed with *System.Uri*. -// - -// -// Rename the field, or change the field type to *System.Uri*. -// -// By default issues of this rule have a **Low** severity -// because they reflect more an advice than a problem. -//]]> - Types should not extend System.ApplicationException -// ND2210:TypesShouldNotExtendSystemApplicationException - -warnif count > 0 from t in Application.Types where - t.DeriveFrom("System.ApplicationException".AllowNoMatch()) -select new { - t, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// At .NET Framework version 1 time, it was -// recommended to derive new exceptions from -// *ApplicationException*. -// -// The recommendation has changed and new -// exceptions should derive from **System.Exception** -// or one of its subclasses in the *System* namespace. -// -// This rule matches application exception classes -// that derive from *ApplicationException*. -// - -// -// Make sure that matched exception types, -// derive from **System.Exception** or one of its -// subclasses in the *System* namespace. -//]]> - Don't Implement ICloneable -// ND2211:DontImplementICloneable - -warnif count > 0 -from t in Application.Types -where t.Implement("System.ICloneable".AllowNoMatch()) - - // Don't warn for classes that derive from a class that implements ICloneable - && !t.BaseClasses.Any(c => c.Implement("System.ICloneable".AllowNoMatch())) - -select new { - t, - Debt = 1.ToHours().ToDebt(), - Severity = Severity.High -} - -// -// The interface *System.ICloneable* is proposed since the first version of .NET. -// It is considered as a bad API for several reasons. -// -// First, this interface existed even before .NET 2.0 that introduced generics. -// Hence this interface is not generic and the *Clone()* method returns an *object* -// reference that needs to be *downcasted* to be useful. -// -// Second, this interface doesn't make explicit whether the cloning should be -// **deep** or **shallow**. The difference between the two behaviors -// is fundamental (explanation here https://stackoverflow.com/a/184745/27194). -// Not being able to make the intention explicit leads to confusion. -// -// Third, classes that derive from a class that implements *ICloneable* -// **must** also implement the *Clone()* method, and it is easy to forget -// this constraint. -// -// This rule doesn't warn for classes that derive from a class that -// implements ICloneable. In this case the issue must be fixed -// in the base class, or is maybe not fixable if the base class -// is declared in a third-party assembly. -// - -// -// Don't implement anymore this interface. -// -// You can rename the remaining *Clone()* methods to -// *DeepClone()* or *ShallowClone()* with a typed result. -// -// Or you can propose two custom generic interfaces -// *IDeepCloneable* with the single method *DeepClone():T* -// and *IShallowCloneable* with the single method *ShallowClone():T*. -// -// Finally you can write custom NDepend rules to make sure -// that all classes that derive from a class with a virtual -// clone method, override this method. -//]]> - - - Collection properties should be read only -// ND2300:CollectionPropertiesShouldBeReadOnly - -warnif count > 0 - -// First find collectionTypes -let collectionInterfaces = ThirdParty.Types.WithFullNameIn( - "System.Collections.ICollection", - "System.Collections.Generic.ICollection") -where collectionInterfaces.Count() > 0 -let collectionTypes = Types.ThatImplementAny(collectionInterfaces) - .Union(collectionInterfaces) - .ToHashSetEx() - -// Then find all property setters that have an associated -// getter that returns a collection type. -from propGetter in JustMyCode.Methods.Where( - m => m.IsPropertyGetter && - m.ReturnType != null && - collectionTypes.Contains(m.ReturnType)) - -let propSetter = propGetter.ParentType.Methods.WithSimpleName( - propGetter.SimpleName.Replace("get_","set_") - ).FirstOrDefault() - -where propSetter != null && - !propSetter.IsPrivate && // Ignore private setters since this is private implementation detail - !propSetter.IsInternal && - !propSetter.ParentType.IsPrivate && // Ignore setters of private types - !propSetter.IsPropertyInit && - // Ignore properties of serializable types - !propSetter.ParentType.TypesUsed.Any(t1 => t1.IsAttributeClass && t1.ParentNamespace.Name == "Newtonsoft.Json") && - !propSetter.ParentType.HasAttribute("System.Runtime.Serialization.DataContractAttribute".AllowNoMatch()) && - !propSetter.ParentType.HasAttribute("System.Xml.Serialization.XmlRootAttribute".AllowNoMatch()) - -select new { - propSetter, - CollectionType = propGetter.ReturnType, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// A writable collection property allows a user to replace the collection with -// a completely different collection. A read-only property stops the collection -// from being replaced but still allows the individual members to be set. If -// replacing the collection is a goal, the preferred *design pattern* is to include -// a method to remove all the elements from the collection and a method to -// re-populate the collection. See the *Clear()* and *AddRange()* methods of the -// *System.Collections.Generic.List* class for an example of this pattern. -// -// Both binary and XML serialization support read-only properties that are -// collections. The *System.Xml.Serialization.XmlSerializer* class has specific -// requirements for types that implement *ICollection* and *System.Collections.IEnumerable* -// in order to be serializable. -// -// This rule matches property setter methods that assign a collection object. -// - -// -// To fix a violation of this rule, make the property read-only and, if -// the design requires it, add methods to clear and re-populate the collection. -//]]> - Don't use .NET 1.x HashTable and ArrayList (deprecated) -// ND2301:DontUseDotNET1HashTableAndArrayList - -warnif count > 0 -let forbiddenTypes = ThirdParty.Types.WithFullNameIn( - "System.Collections.HashTable", - "System.Collections.ArrayList", - "System.Collections.Queue", - "System.Collections.Stack", - "System.Collections.SortedList") -where forbiddenTypes.Count() > 0 -from m in Application.Methods.ThatCreateAny(forbiddenTypes) -let forbiddenTypesUsed = m.MethodsCalled.Where(m1 => m1.IsConstructor && forbiddenTypes.Contains(m1.ParentType)).ParentTypes() -select new { - m, - forbiddenTypesUsed, - Debt = 15.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule has been deprecated and, as a consequence, it is disabled by default. -// Feel free to re-enable it if it makes sense in your dev environment. -// -// This rule warns about application methods that use a non-generic -// collection class, including **ArrayList**, **HashTable**, **Queue**, -// **Stack** or **SortedList**. -// - -// -// **List** should be preferred over **ArrayList**. -// It is generic hence you get strongly typed elements. -// Also, it is faster with *T* as a value types since it avoids boxing. -// -// For the same reasons: -// -// • **Dictionary** should be prevered over **HashTable**. -// -// • **Queue** should be prevered over **Queue**. -// -// • **Stack** should be prevered over **Stack**. -// -// • **SortedDictionary** or **SortedList** should be prevered over **SortedList**. -// -// You can be forced to use *non generic* collections -// because you are using third party code that requires -// working with these classes or because you are -// coding with .NET 1.x, but nowadays this situation should -// question about using newer updates of .NET. -// .NET 1.x is an immature platform conpared to newer .NET -// updates. -//]]> - Caution with List.Contains() -// ND2302:CautionWithListContains - -// warnif count > 0 // This query is n ot a rule per default - -let containsMethods = ThirdParty.Methods.WithFullNameIn( - "System.Collections.Generic.List.Contains(T)", - "System.Collections.Generic.IList.Contains(T)", - "System.Collections.ArrayList.Contains(Object)") - -from m in Application.Methods.UsingAny(containsMethods) -select new { - m, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This code query matches calls to *List.Contains()* method. -// -// The cost of checking if a list contains an object is proportional -// to the size of the list. In other words it is a *O(N)* operation. -// For large lists and/or frequent calls to *Contains()*, prefer using -// the *System.Collections.Generic.HashSet* class -// where calls to *Contains()* take a constant -// time (*O(0)* operation). -// -// This code query is not a code rule, because more often than not, -// calling *O(N) Contains()* is not a mistake. This code query -// aims at pointing out this potential performance pitfall. -//]]> - Prefer return collection abstraction instead of implementation -// ND2303:PreferReturnCollectionAbstractionInsteadOfImplementation - -// warnif count > 0 // This query is n ot a rule per default - -let implTypes = ThirdParty.Types.WithFullNameIn( - "System.Collections.Generic.List", - "System.Collections.Generic.HashSet", - "System.Collections.Generic.Dictionary") - -from m in Application.Methods.WithReturnTypeIn(implTypes) -select new { - m, - m.ReturnType, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Low -} - -// -// This code query matches methods that return a -// collection implementation, such as *List* -// *HashSet* or *Dictionary*. -// -// Most often than not, clients of a method don't -// need to know the exact implementation of the -// collection returned. It is preferable to return -// a collection interface such as *IList*, -// *ICollection*, *IEnumerable* or -// *IDictionary*. -// -// Using the collection interface instead of the -// implementation shouldn't applies to all cases, -// hence this code query is not code rule. -//]]> - - - P/Invokes should be static and not be publicly visible -// ND2400:PInvokesShouldBeStaticAndNotBePubliclyVisible - -warnif count > 0 from m in Application.Methods where - !m.IsThirdParty && - (m.HasAttribute ("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch())) && - ( m.IsPubliclyVisible || - !m.IsStatic) -select new { - m, - m.Visibility, - m.IsStatic, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// Methods that are marked with the **DllImportAttribute** attribute -// (or methods that are defined by using the **Declare** keyword in Visual Basic) -// use **Platform Invocation Services** to access unmanaged code. -// -// Such methods should not be exposed. By keeping these methods *private* or *internal*, -// you make sure that your library cannot be used to breach security by allowing -// callers access to unmanaged APIs that they could not call otherwise. -// -// This rule matches methods tagged with *DllImportAttribute* attribute -// that are declared as *public* or declared as *non-static*. -// - -// -// To fix a violation of this rule, change the access level of the method -// and/or declare it as static. -//]]> - Move P/Invokes to NativeMethods class -// ND2401:MovePInvokesToNativeMethodsClass - -warnif count > 0 from m in Application.Methods where - m.HasAttribute ("System.Runtime.InteropServices.DllImportAttribute".AllowNoMatch()) && - m.ParentType.SimpleName != "NativeMethods" && - !m.FullName.Contains("Frameworks") && - !m.FullName.Contains("WebCam") -select new { - m, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// **Platform Invocation methods**, such as those that are marked by using the -// **System.Runtime.InteropServices.DllImportAttribute** attribute, or methods -// that are defined by using the **Declare** keyword in Visual Basic, access -// unmanaged code. These methods should be in one of the following classes: -// -// • **NativeMethods** - This class does not suppress stack walks for unmanaged -// code permission. (*System.Security.SuppressUnmanagedCodeSecurityAttribute* -// must not be applied to this class.) This class is for methods that can be -// used anywhere because a stack walk will be performed. -// -// • **SafeNativeMethods** - This class suppresses stack walks for unmanaged -// code permission. (*System.Security.SuppressUnmanagedCodeSecurityAttribute* -//is applied to this class.) This class is for methods that are safe for anyone -// to call. Callers of these methods are not required to perform a full security -// review to make sure that the usage is secure because the methods are harmless -// for any caller. -// -// • **UnsafeNativeMethods** - This class suppresses stack walks for unmanaged -// code permission. (*System.Security.SuppressUnmanagedCodeSecurityAttribute* -// is applied to this class.) This class is for methods that are potentially -// dangerous. Any caller of these methods must perform a full security review -// to make sure that the usage is secure because no stack walk will be performed. -// -// These classes are declared as *static internal*. The methods in these -// classes are *static* and *internal*. -// -// This rule matches *P/Invoke* methods not declared in such *NativeMethods* -// class. -// - -// -// To fix a violation of this rule, move the method to the appropriate -// **NativeMethods** class. For most applications, moving P/Invokes to a new -// class that is named **NativeMethods** is enough. -//]]> - NativeMethods class should be static and internal -// ND2402:NativeMethodsClassShouldBeStaticAndInternal - -warnif count > 0 from t in Application.Types.WithNameIn( - @"NativeMethods", "SafeNativeMethods", "UnsafeNativeMethods") where - t.IsPublic || !t.IsStatic -select new { - t, - t.Visibility, - t.IsStatic, - Debt = 5.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// In the description of the default rule *Move P/Invokes to NativeMethods class* -// it is explained that *NativeMethods* classes that host *P/Invoke* methods, -// should be declared as *static* and *internal*. -// -// This code rule warns about *NativeMethods* classes that are not declared -// *static* and *internal*. -// - -// -// Matched *NativeMethods* classes must be declared as *static* and *internal*. -//]]> - - - Don't create threads explicitly -// ND2500:DontCreateThreadsExplicitly - -warnif count > 0 from m in Application.Methods where - m.ParentType.Name != "ThreadExtensions" && - !m.FullName.Contains("VideoInputs") && - m.CreateA ("System.Threading.Thread".AllowNoMatch()) -select new { - m, - Debt = 60.ToMinutes().ToDebt(), - Severity = Severity.Critical -} - -// -// This code rule warns about methods that create *threads* explicitly -// by creating an instance of the class *System.Threading.Thread*. -// -// Prefer using the thread pool instead of creating manually your -// own threads. Threads are costly objects. They take approximately -// 200,000 cycles to create and about 100,000 cycles to destroy. -// By default they reserve 1 Mega Bytes of virtual memory for its -// stack and use 2,000-8,000 cycles for each context switch. -// -// As a consequence, it is preferable to let the thread pool -// recycle threads. -// - -// -// Instead of creating explicitly threads, use the **Task Parralel -// Library** *(TPL)* that relies on the CLR thread pool. -// -// Introduction to TPL: https://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx -// -// TPL and the CLR v4 thread pool: -// http://www.danielmoth.com/Blog/New-And-Improved-CLR-4-Thread-Pool-Engine.aspx -// -// By default issues of this rule have a **Critical** severity -// because creating threads can have severe consequences. -//]]> - Don't use dangerous threading methods -// ND2501:DontUseDangerousThreadingMethods - -warnif count > 0 - -let wrongMethods = ThirdParty.Methods.WithFullNameIn( - - "System.Threading.Thread.Abort()", - "System.Threading.Thread.Abort(Object)", - - "System.Threading.Thread.Sleep(Int32)", - - "System.Threading.Thread.Suspend()", - "System.Threading.Thread.Resume()") - -from m in Application.Methods.UsingAny(wrongMethods) where -!m.ParentType.FullName.Contains("ThreadExtensions") && -m.ParentType.Name != "ThreadExtensionsTests" && -m.ParentType.Name != "ThreadStaticTests" && -m.ParentType.Name != "ContentDownloadTests" && -m.ParentType.Name != "TestRealAuthenticationProvider" && -!m.Name.EndsWith("Stopwatch()") && -!m.Name.Contains("Performance") && -!m.FullName.Contains("MultithreadedTests") && -!m.FullName.Contains("AppRunner") && -!m.FullName.Contains("AutofacContentResolver") && -!m.FullName.Contains("Network") && -!m.FullName.Contains("Simulate") && -!m.FullName.Contains("LoadAssembly") && -!m.Name.Contains("AndWait") && -m.ParentType.Name != "Program" && -m.ParentType.Name != "ContentFileManipulator" && -m.ParentType.Name != "SolutionProjectsTests" && -!m.ParentType.FullName.Contains("VideoInput") && -!m.Name.Contains("WaitUntilContentReadyReceivedOrServerErrorHappened") && -!m.Name.Contains("Wait10msForAppDomainCleanup") && -m.Name != "CopyFileAndRetryIfLocked(String,String)" && -m.Name != "TryOpenLogFileInEditor()" && -m.Name != "Connect(Action,Action)" && -m.Name != "ReadAllBytes(String)" && -!m.Name.Contains("WhileTheFileIsStillBeingUsed") && -!m.Name.EndsWith("AfterDelay()") -select new { - m, - suppressCallsTo = m.MethodsCalled.Intersect(wrongMethods), - Debt = 40.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns about using the methods -// *Abort()*, *Sleep()*, *Suspend()* or *Resume()* -// declared by the *Thread* class. -// -// • Usage of *Thread.Abort()* is dangerous. -// More information on this here: -// http://www.interact-sw.co.uk/iangblog/2004/11/12/cancellation -// -// • Usage of *Thread.Sleep()* is a sign of -// flawed design. More information on this here: -// http://msmvps.com/blogs/peterritchie/archive/2007/04/26/thread-sleep-is-a-sign-of-a-poorly-designed-program.aspx -// -// • *Suspend()* and *Resume()* are dangerous threading methods, marked as obsolete. -// More information on workaround here: -// http://stackoverflow.com/questions/382173/what-are-alternative-ways-to-suspend-and-resume-a-thread -// - -// -// Suppress calls to *Thread* methods exposed in the -// *suppressCallsTo* column in the rule result. -// -// Use instead facilities offered by the **Task Parralel -// Library** *(TPL)* : -// https://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx -//]]> - Monitor TryEnter/Exit must be both called within the same method -// ND2502:MonitorTryEnterExitMustBeBothCalledWithinTheSameMethod - -warnif count > 0 - -let enterMethods = ThirdParty.Methods.WithFullNameWildcardMatchIn( - "System.Threading.Monitor.Enter(*", - "System.Threading.Monitor.TryEnter(*") - -from m in Application.Methods.UsingAny(enterMethods) -where - !m.IsUsing ("System.Threading.Monitor.Exit(Object)".AllowNoMatch()) -select new { - m, - enterMethodsCalled = m.MethodsCalled.Intersect(enterMethods), - Debt = 20.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns when **System.Threading.Monitor** *Enter()* -// (or *TryEnter()*) and *Exit() methods are not called within -// the same method. -// -// Doing so makes the code *less readable*, because it gets harder -// to locate when **critical sections** begin and end. -// -// Also, you expose yourself to complex and error-prone scenarios. -// - -// -// Refactor matched methods to make sure that *Monitor critical -// sections* begin and end within the same method. Basics scenarios -// can be handled through the C# **lock** keyword. Using explicitly -// the class *Monitor* should be left for advanced situations, -// that require calls to methods like *Wait()* and *Pulse()*. -// -// More information on using the *Monitor* class can be found here: -// http://www.codeproject.com/Articles/13453/Practical-NET-and-C-Chapter -//]]> - ReaderWriterLock AcquireLock/ReleaseLock must be both called within the same method -// ND2503:ReaderWriterLockAcquireLockReleaseLockMustBeBothCalledWithinTheSameMethod - -warnif count > 0 - -let acquireLockMethods = ThirdParty.Methods.WithFullNameWildcardMatch( - "System.Threading.ReaderWriterLock.Acquire*Lock(*") - -let releaseLockMethods = ThirdParty.Methods.WithFullNameWildcardMatch( - "System.Threading.ReaderWriterLock.Release*Lock(*") - -from m in Application.Methods.UsingAny(acquireLockMethods) - .Except(Application.Methods.UsingAny(releaseLockMethods)) -select new { - m, - acquireLockMethods = m.MethodsCalled.Intersect(acquireLockMethods), - Debt = 20.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns when **System.Threading.ReaderWriterLock** -// acquire and release, reader or writer locks methods are not called -// within the same method. -// -// Doing so makes the code *less readable*, because it gets harder -// to locate when **critical sections** begin and end. -// -// Also, you expose yourself to complex and error-prone scenarios. -// - -// -// Refactor matched methods to make sure that *ReaderWriterLock -// read or write critical sections* begin and end within the -// same method. -//]]> - Don't tag instance fields with ThreadStaticAttribute -// ND2504:DontTagInstanceFieldsWithThreadStaticAttribute - -warnif count > 0 -from f in Application.Fields -where !f.IsStatic && - f.HasAttribute ("System.ThreadStaticAttribute".AllowNoMatch()) -select new { - f, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// This rule warns when the attribute **System.ThreadStaticAttribute** -// is tagging *instance* fields. As explained in documentation, this attribute -// is designed to tag only *static* fields. -// https://msdn.microsoft.com/en-us/library/system.threadstaticattribute -// - -// -// Refactor the code to make sure that all fields tagged with -// *ThreadStaticAttribute* are *static*. -//]]> - Method non-synchronized that read mutable states -// ND2505:MethodNonSynchronizedThatReadMutableStates - -from m in Application.Methods where - (m.ReadsMutableObjectState || m.ReadsMutableTypeState) && - !m.IsUsing ("System.Threading.Monitor".AllowNoMatch()) && - !m.IsUsing ("System.Threading.ReaderWriterLock".AllowNoMatch()) -select new { - m, - mutableFieldsUsed = m.FieldsUsed.Where(f => !f.IsImmutable) -} - -// -// Mutable object states are instance fields that -// can be modified through the lifetime of the object. -// -// Mutable type states are static fields that can be -// modified through the lifetime of the program. -// -// This query lists methods that read mutable state -// without synchronizing access. In the case of -// multi-threaded program, doing so can lead to -// state corruption. -// -// This code query is not a code rule because more often -// than not, a match of this query is not an issue. -//]]> - - - Method should not return concrete XmlNode -// ND2600:MethodShouldNotReturnConcreteXmlNode - -warnif count > 0 - -let concreteXmlTypes = ThirdParty.Types.ThatDeriveFromAny( - ThirdParty.Types.WithFullName("System.Xml.XmlNode")) - -from m in Application.Methods.WithReturnTypeIn(concreteXmlTypes) -select new { - m, - m.ReturnType, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns about method whose return type is -// **System.Xml.XmlNode** or any type derived from *XmlNode*. -// -// *XmlNode* implements the interface **System.Xml.Xpath.IXPathNavigable**. -// In most situation, returning this interface instead of the concrete -// type is a better *design* choice that will abstract client code -// from implementation details. -// - -// -// To fix a violation of this rule, change the concrete returned type -// to the suggested interface *IXPathNavigable* and refactor clients -// code if possible. -//]]> - Types should not extend System.Xml.XmlDocument -// ND2601:TypesShouldNotExtendSystemXmlXmlDocument - -warnif count > 0 from t in Application.Types where - t.DeriveFrom("System.Xml.XmlDocument".AllowNoMatch()) -select new { - t, - Debt = 20.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule warns aboud subclasses of **System.Xml.XmlDocument**. -// -// Do not create a subclass of *XmlDocument* if you want to -// create an XML view of an underlying object model or data source. -// - -// -// Instead of subclassing *XmlDocument*, you can use the interface -// **System.Xml.XPath.IXPathNavigable** implemented by the class -// *XmlDocument*. -// -// An alternative of using *XmlDocument*, is to use -// **System.Xml.Linq.XDocument**, aka **LINQ2XML**. -// More information on this can be found here: -// http://stackoverflow.com/questions/1542073/xdocument-or-xmldocument -//]]> - - - Float and Date Parsing must be culture aware -// ND2700:FloatAndDateParsingMustBeCultureAware - -warnif count > 0 - -let cultureUnawareMethods = - (from m in ThirdParty.Types.WithFullNameIn( - "System.DateTime", - "System.Single", - "System.Double", - "System.Decimal").ChildMethods() -let methodsCallingMe = m.MethodsCallingMe.Where(mm => mm.ParentType.Name == "DateTime") - - where m.NbParameters > 0 && - methodsCallingMe.Count() > 0 && - (m.SimpleName.EqualsAny( - "Parse", "TryParse", "ToString")) && - !m.Name.Contains("IFormatProvider") - select m).ToHashSetEx() - -from m in Application.Methods.UsingAny(cultureUnawareMethods) -let cultureUnawareMethodsCalled = m.MethodsCalled.Intersect(cultureUnawareMethods) -select new { - m, - shouldntCall = cultureUnawareMethodsCalled, - Debt = (5 + 3*cultureUnawareMethodsCalled.Count()).ToMinutes().ToDebt(), - Severity = 5*cultureUnawareMethodsCalled.Count().ToMinutes().ToAnnualInterest() -} - -// -// Globalization is the design and development of applications that support -// localized user interfaces and regional data for users in multiple cultures. -// -// This rule warns about the usage of *non-globalized overloads* of -// the methods **Parse()**, **TryParse()** and **ToString()**, -// of the types **DateTime**, **float**, **double** and **decimal**. -// This is the symptom that your application is *at least partially* -// not globalized. -// -// *Non-globalized overloads* of these methods are the overloads -// that don't take a parameter of type **IFormatProvider**. -// - -// -// Globalize your applicaton and make sure to use the globalized overloads -// of these methods. In the column **MethodsCallingMe** of this rule result -// are listed the methods of your application that call the -// *non-globalized overloads*. -// -// More information on **Creating Globally Aware Applications** here: -// https://msdn.microsoft.com/en-us/library/cc853414(VS.95).aspx -// -// The estimated Debt, which means the effort to fix such issue, -// is equal to 5 minutes per application method calling at least one -// non-culture aware method called, plus 3 minutes per non-culture aware -// method called. -//]]> - - - Assemblies should have the same version -// ND2801:AssembliesShouldHaveTheSameVersion - -warnif count > 0 -let versionsLookup = Application.Assemblies.ToLookup(a => a.Version, a=> a) -let mostRepresentedVersion = versionsLookup.OrderByDescending(v => v.Count()).First().Key -from v in versionsLookup -where v.Key != mostRepresentedVersion -from a in v.ToArray() -select new { - a , - version = v.Key, - mostRepresentedVersion, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// This rule reports application assemblies that have a version different -// than the version shared by most of application assemblies. -// -// Before fixing these issues, double check if there is a valid reason -// for dealing with more than one assembly version number. -// Typically this happens when the analyzed code base is made of assemblies -// that are not *compiled, developed or deployed* together. -// - -// -// If all assemblies of your application should have the same version number, -// just use the attribute **System.Reflection.AssemblyVersion** in a source -// file shared by the assemblies. -// -// Typically this source file is generated by a dedicated *MSBuild* task -// like this one http://www.msbuildextensionpack.com/help/4.0.5.0/html/d6c3b5e8-00d4-c826-1a73-3cfe637f3827.htm. -// -// Here you can find interesting assemblies versioning advices. -// http://stackoverflow.com/a/3905443/27194 -// -// By default issues of this rule have a severity set to **major** since -// unproper assemblies versioning can lead to complicated deployment problem. -//]]> - - - Public methods returning a reference needs a contract to ensure that a non-null reference is returned -// ND2900:PublicMethodsReturningAReferenceNeedsAContractToEnsureThatANonNullReferenceIsReturned - -warnif count > 0 -let ensureMethods = Application.Methods.WithFullName( - "System.Diagnostics.Contracts.__ContractsRuntime.Ensures(Boolean,String,String)") - -from ensureMethod in ensureMethods -from m in ensureMethod.ParentAssembly.ChildMethods where - m.IsPubliclyVisible && - !m.IsAbstract && - m.ReturnType != null && - // Identify that the return type is a reference type - (m.ReturnType.IsClass || m.ReturnType.IsInterface) && - !m.IsUsing(ensureMethod) && - - // Don't match method not implemented yet! - !m.CreateA("System.NotImplementedException".AllowNoMatch()) - -select new { - m, - ReturnTypeReference = m.ReturnType, - Debt = (5+3*m.MethodsCallingMe.Count()).ToMinutes().ToDebt(), - Severity = Severity.Medium -} - -// -// **Code Contracts** are useful to decrease ambiguity between callers and callees. -// Not ensuring that a reference returned by a method is *non-null* leaves ambiguity -// for the caller. This rule matches methods returning an instance of a reference type -// (class or interface) that doesn't use a **Contract.Ensure()** method. -// -// *Contract.Ensure()* is defined in the **Microsoft Code Contracts for .NET** -// library, and is typically used to write a code contract on returned reference: -// *Contract.Ensures(Contract.Result() != null, "returned reference is not null");* -// https://visualstudiogallery.msdn.microsoft.com/1ec7db13-3363-46c9-851f-1ce455f66970 -// - -// -// Use *Microsoft Code Contracts for .NET* on the public surface of your API, -// to remove most ambiguity presented to your client. Most of such ambiguities -// are about *null* or *not null* references. -// -// Don't use *null* reference if you need to define a method that might not -// return a result. Use instead the **TryXXX()** pattern exposed for example -// in the *System.Int32.TryParse()* method. -// -// The estimated Debt, which means the effort to fix such issue, is equal -// to 5 minutes per public application method that might return a null reference -// plus 3 minutes per method calling such method. -//]]> - - - - Discard generated Assemblies from JustMyCode -// ND3000:DiscardGeneratedAssembliesFromJustMyCode - -notmycode -from a in Application.Assemblies where -// Assemblies generated for Xsl IL compilation for example are tagged with this attribute -a.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) -select a - -// -// This code query is prefixed with **notmycode**. -// This means that all application assemblies matched by this -// code query are removed from the *code base view* **JustMyCode.Assemblies**. -// It also means that all *namespaces*, *types*, *methods* and -// *fields* contained in a matched assembly are removed from -// the code base view *JustMyCode*. -// The code base view *JustMyCode* is used by most default code queries -// and rules. -// -// So far this query only matches application assemblies tagged -// with *System.CodeDom.Compiler.GeneratedCodeAttribute*. -// Make sure to make this query richer to discard your generated -// assemblies from the NDepend rules results. -// -// *notmycode* queries are executed before running others -// queries and rules. Also modifying a *notmycode* query -// provokes re-run of queries and rules that rely -// on the *JustMyCode* code base view. -// -// Several *notmycode* queries can be written to match *assemblies*, -// in which case this results in cumulative effect. -// -// Online documentation: -// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode -//]]> - Discard generated Namespaces from JustMyCode -// ND3001:DiscardGeneratedNamespacesFromJustMyCode - -notmycode - -// First gather assemblies written with VB.NET -let vbnetAssemblies = Application.Assemblies.Where( - a => a.SourceDecls.Any(decl => decl.SourceFile.FileNameExtension.ToLower() == ".vb")) -// Then find the My namespace and its child namespaces. -let vbnetMyNamespaces = vbnetAssemblies.ChildNamespaces().Where( - n => n.SimpleName == "My" || - n.ParentNamespaces.Any(nParent => nParent.SimpleName == "My")) - - -let generatedNamespace = Application.Namespaces.WithFullNameIn( - // COM/Import mshtml namespace and all its types are not-my-code - "mshtml", - // Roslyn can generate types in these namespaces, consider them as not-my-code - "Microsoft.CodeAnalysis", - "System.Runtime.CompilerServices" - -).Concat(Application.Namespaces.WithSimpleNameIn( - // Don't match types generated by the Refit infrastructure like the class PreserveAttribute - "RefitInternalGenerated")) - -// Discard types generated in Microsoft.Office.* namespaces. -// Issues won't be reported on this code. Especially issues related -// to the rule "Interface name should begin with a 'I'" broken -// by many interfaces in these namespaces. -let microsoftOffices = Application.Namespaces.Where(n => n.Name.StartsWith("Microsoft.Office")) - -from n in vbnetMyNamespaces.Concat(generatedNamespace).Concat(microsoftOffices) -select n - -// -// This code query is prefixed with **notmycode**. -// This means that all application namespaces matched by this -// code query are removed from the *code base view* **JustMyCode.Namespaces**. -// It also means that all *types*, *methods* and *fields* contained in a -// matched namespace are removed from the code base view *JustMyCode*. -// The code base view *JustMyCode* is used by most default code queries -// and rules. -// -// So far this query matches the **My** namespaces generated -// by the VB.NET compiler. This query also matches namespaces named -// **mshtml** or **Microsoft.Office.*** that are generated by tools. -// -// *notmycode* queries are executed before running others -// queries and rules. Also modifying a *notmycode* query -// provokes re-run of queries and rules that rely -// on the *JustMyCode* code base view. -// -// Several *notmycode* queries can be written to match *namespaces*, -// in which case this results in cumulative effect. -// -// Online documentation: -// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode -//]]> - Discard generated Types from JustMyCode -// ND3002:DiscardGeneratedTypesFromJustMyCode - -notmycode - -// Define some sets to quickly test EntityFramework generated types -let efContexts = Application.Types.Where(t => - t.DeriveFrom("System.Data.Entity.DbContext".AllowNoMatch()) && - t.SourceDecls.Count(sd => sd.SourceFile.FileName.ToLower().EndsWithAny(".context.cs", ".context.vb")) == 1).ToHashSetEx() -let efEntities = Application.Types.UsedByAny(efContexts).ToHashSetEx() -let efMigrations = Application.Types.Where(t => t.DeriveFrom("System.Data.Entity.Migrations.DbMigration".AllowNoMatch())).ToHashSetEx() -let efModelSnapshots = Application.Types.Where(t => t.DeriveFrom("Microsoft.EntityFrameworkCore.Infrastructure.ModelSnapshot".AllowNoMatch())).ToHashSetEx() - -from t in Application.Types where - - // Don't consider anonymous types as JustMyCode - // C# and VB.NET anonymous types generated by the compiler satisfies these conditions - (t.IsGeneratedByCompiler && - t.ParentNamespace.Name.Length == 0 && - t.SimpleNameLike("AnonymousType")) || - - // Resources, Settings, or typed DataSet generated types for example, are tagged with this attribute - t.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) || - - // This attribute identifies a type or member that is not part of the user code for an application. - t.HasAttribute ("System.Diagnostics.DebuggerNonUserCodeAttribute".AllowNoMatch()) || - - // This attribute identifies a type that is defined with COM, elsewhere, so consider it not-my-code. - t.HasAttribute ("System.Runtime.InteropServices.GuidAttribute".AllowNoMatch()) || - - // Delegate types are always generated - t.IsDelegate || - - // Discard ASP.NET page types generated by aspnet_compiler.exe - // See: https://www.ndepend.com/FAQ.aspx#ASPNET - t.ParentNamespace.Name.EqualsAny("ASP", "__ASP") || - - // Discard ASP.NET special types - (t.SimpleName.EqualsAny("Startup","BundleConfig","RouteConfig") && - ThirdParty.Assemblies.WithNameWildcardMatchIn("System.Web*", "Microsoft.AspNetCore*").Any()) || - - // Discard DataSet classes and their nested types - (t.DeriveFrom("System.Data.DataSet".AllowNoMatch()) && - t.HasAttribute("System.ComponentModel.DesignerCategoryAttribute".AllowNoMatch())) || - (t.IsNested && t.ParentType != null && - t.ParentType.DeriveFrom("System.Data.DataSet".AllowNoMatch()) && - t.ParentType.HasAttribute("System.ComponentModel.DesignerCategoryAttribute".AllowNoMatch())) || - - // Discard DataSet TableAdapterManager classes their nested types - (t.SimpleName == "TableAdapterManager" && - t.DeriveFrom("System.ComponentModel.Component".AllowNoMatch())) || - (t.IsNested && t.ParentType != null && - t.ParentType.SimpleName == "TableAdapterManager" && - t.ParentType.DeriveFrom("System.ComponentModel.Component".AllowNoMatch())) || - - // Discard Xamarin form generated types that contain the method LoadDataTemplate() - (t.IsNested && - t.SimpleName.StartsWith("") && - t.Methods.Count(m => m.SimpleName == "LoadDataTemplate") == 1 && - t.ParentAssembly.AssembliesUsed.Count(a => a.Name.StartsWith("Xamarin")) > 0) || - - // Discard Xamarin Resource types - (t.IsNested && - t.ParentType != null && - t.ParentType.Name == "Resource" && - t.ParentAssembly.AssembliesUsed.Count(a => a.Name.StartsWith("Xamarin")) > 0) || - - // Discard Entity Framework generated DB context types and entities types used by DB context types! - efContexts.Contains(t) || - efEntities.Contains(t) || - efMigrations.Contains(t) || - efModelSnapshots.Contains(t) || - - // Discard types generated for code contract - t.FullName.StartsWith("System.Diagnostics.Contracts.__ContractsRuntime") || - t.FullName == "System.Diagnostics.Contracts.RuntimeContractsAttribute" || - - // Discard all types declared in a folder path containing the word "generated" - (t.SourceFileDeclAvailable && - t.SourceDecls.All(s => s.SourceFile.FilePath.ParentDirectoryPath.ToString().ToLower().Contains("generated"))) || - - // Types created by the test infrastructure - t.FullName == "AutoGeneratedProgram" || - - // Discard special types generated by the Roslyn compiler that are typically used by several methods - // and as a consequence, cannot be merged in application code through the option - // NDepend Project Properties > Analysis > Merge Code Generated by Compiler into Application Code - (t.IsGeneratedByCompiler && t.IsNested && t.SimpleName.StartsWith("<>c")) - -select t - -// -// This code query is prefixed with **notmycode**. -// This means that all application types matched by this -// code query are removed from the *code base view* **JustMyCode.Types**. -// It also means that all *methods* and *fields* contained in a -// matched type are removed from the code base view *JustMyCode*. -// The code base view *JustMyCode* is used by most default code queries -// and rules. -// -// So far this query matches several well-identified generated -// types, like the ones tagged with *System.CodeDom.Compiler.GeneratedCodeAttribute*. -// Make sure to make this query richer to discard your generated -// types from the NDepend rules results. -// -// *notmycode* queries are executed before running others -// queries and rules. Also modifying a *notmycode* query -// provokes re-run of queries and rules that rely -// on the *JustMyCode* code base view. -// -// Several *notmycode* queries can be written to match *types*, -// in which case this results in cumulative effect. -// -// Online documentation: -// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode -//]]> - Discard generated and designer Methods from JustMyCode -// ND3003:DiscardGeneratedAndDesignerMethodsFromJustMyCode - -notmycode - -// -// First define source files paths to discard -// -from a in Application.Assemblies -where a.SourceFileDeclAvailable -let asmSourceFilesPaths = a.SourceDecls.Select(s => s.SourceFile.FilePath) - -let sourceFilesPathsToDiscard = ( - from filePath in asmSourceFilesPaths - let filePathLower= filePath.ToString().ToLower() - where - filePathLower.EndsWithAny( - ".g.cs", // Popular pattern to name generated files. - ".g.vb", - ".generated.cs", - ".generated.vb") || - filePathLower.EndsWithAny( - ".xaml", // notmycode WPF xaml code - ".designer.cs", // notmycode C# Windows Forms designer code - ".designer.vb") // notmycode VB.NET Windows Forms designer code - || - // notmycode methods in source files in a directory containing generated - filePathLower.Contains("generated") - select filePath -).ToHashSetEx() - -// -// Second: discard methods in sourceFilesPathsToDiscard -// -from m in a.ChildMethods -where (m.SourceFileDeclAvailable && - sourceFilesPathsToDiscard.Contains(m.SourceDecls.First().SourceFile.FilePath)) || - // Generated methods might be tagged with this attribute - m.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) || - - // This attributes identifies a type or member that is not part of the user code for an application. - m.HasAttribute ("System.Diagnostics.DebuggerNonUserCodeAttribute".AllowNoMatch()) || - - // Event adder/remover methods generated by the compiler. - ((m.IsEventAdder || m.IsEventRemover) && !m.SourceFileDeclAvailable) || - - // Default/implicit constructor generated by the compiler on class and structures that don't have constructor - (m.IsConstructor && - m.NbParameters == 0 && - (m.IsPublic || (m.IsProtected && m.ParentType.IsAbstract)) && - !m.SourceFileDeclAvailable) - -select new { m, m.NbLinesOfCode } - -// -// This code query is prefixed with **notmycode**. -// This means that all application methods matched by this -// code query are removed from the *code base view* **JustMyCode.Methods**. -// The code base view *JustMyCode* is used by most default code queries -// and rules. -// -// So far this query matches several well-identified generated -// methods, like the ones tagged with *System.CodeDom.Compiler.GeneratedCodeAttribute*, -// or the ones declared in a source file suffixed with *.designer.cs*. -// Make sure to make this query richer to discard your generated -// methods from the NDepend rules results. -// -// *notmycode* queries are executed before running others -// queries and rules. Also modifying a *notmycode* query -// provokes re-run of queries and rules that rely -// on the *JustMyCode* code base view. -// -// Several *notmycode* queries can be written to match *methods*, -// in which case this results in cumulative effect. -// -// Online documentation: -// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode -//]]> - Discard generated Fields from JustMyCode -// ND3004:DiscardGeneratedFieldsFromJustMyCode - -notmycode - -// Define WindowsForm fields defined as fields assigned by the InitializeComponent() WindowsForm methods. -let winFormInitializeComponentsMethods = - Application.Methods.WithName("InitializeComponent()") - .Where(m => m.ParentType.BaseClass != null && m.ParentType.BaseClass.ParentNamespace.Name == "System.Windows.Forms") -let winFormFields = winFormInitializeComponentsMethods.SelectMany(m => m.FieldsAssigned).ToHashSetEx() - -from f in Application.Fields where - - // Discard WindowsForm fields - winFormFields.Contains(f) || - - // Eliminate "components" generated in Windows Form Control context - (f.Name == "components" && f.ParentType.DeriveFrom("System.Windows.Forms.Control".AllowNoMatch())) || - - // Eliminate tagHelper generated fields - f.Name == "_tagHelperStringValueBuffer" || - - // Eliminate XAML generated fields - // IComponentConnector is XAML specific and is automatically implemented for every Window, Page and UserControl. - f.ParentType.Implement( "System.Windows.Markup.IComponentConnector".AllowNoMatch()) || // WPF IComponentConnector - f.ParentType.Implement("Windows.UI.Xaml.Markup.IComponentConnector".AllowNoMatch()) || // UWP IComponentConnector - - f.HasAttribute ("System.CodeDom.Compiler.GeneratedCodeAttribute".AllowNoMatch()) || - - // Property backing fields generated by the compiler - (f.IsGeneratedByCompiler && !f.IsEventDelegateObject) || - - // Match fields generated by the ASP.NET infrastructure - // in System.Web.UI classes like Page, Control, MasterPage... - (f.FieldType != null && - f.ParentType.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWith("System.Web.UI")) && - (f.FieldType.ParentNamespace.Name.StartsWith("System.Web.UI") || - f.FieldType.BaseClasses.Any(bc => bc.ParentNamespace.Name.StartsWith("System.Web.UI")) - )) || - - // Match fields named 'mappingSource' for DataContext classes - (f.Name == "mappingSource" && f.ParentType.DeriveFrom("System.Data.Linq.DataContext".AllowNoMatch())) - -select f - -// -// This code query is prefixed with **notmycode**. -// This means that all application fields matched by this -// code query are removed from the *code base view* **JustMyCode.Fields**. -// The code base view *JustMyCode* is used by most default code queries -// and rules. -// -// This query matches application fields tagged -// with *System.CodeDom.Compiler.GeneratedCodeAttribute*, and -// *Windows Form*, *WPF* and *UWP* fields generated by the designer. -// Make sure to make this query richer to discard your generated -// fields from the NDepend rules results. -// -// *notmycode* queries are executed before running others -// queries and rules. Also modifying a *notmycode* query -// provokes re-run of queries and rules that rely -// on the *JustMyCode* code base view. -// -// Several *notmycode* queries can be written to match *fields*, -// in which case this results in cumulative effect. -// -// Online documentation: -// https://www.ndepend.com/docs/cqlinq-syntax#NotMyCode -//]]> - JustMyCode code elements -from elem in JustMyCode.CodeElements -select new { - elem, - loc = elem.IsCodeContainer ? elem.AsCodeContainer.NbLinesOfCode : null -} - -// -// This code query enumerates all -// *assemblies*, *namespaces*, *types*, *methods* and *fields* -// in your application, that are considered as being your code. -// -// This means concretely that the *ICodeBaseView* **JustMyCode** -// only shows these code elements. This code base view is used by -// many default code rule to avoid being warned on code elements -// that you don't consider as your code - typically the code -// elements generated by a tool. -// -// These code elements are the ones that are not matched -// by any quere prefixed with **notmycode**. -//]]> - NotMyCode code elements -from elem in Application.CodeElements.Where(element => !JustMyCode.Contains(element)) -select new { - elem, - loc = elem.IsCodeContainer ? elem.AsCodeContainer.NbLinesOfCode : null -} - -// -// This code query enumerates all -// *assemblies*, *namespaces*, *types*, *methods* and *fields* -// in your application, that are considered as not being your code. -// -// This means concretely that the *ICodeBaseView* **JustMyCode** -// hide these code elements. This code base view is used by -// many default code rules to avoid being warned on code elements -// that you don't consider as your code - typically the code -// elements generated by a tool. -// -// These code elements are the ones matched by queries prefixed with -// **notmycode**. -//]]> - - - - -from issue in Issues -where issue.WasAdded() -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }]]> - -from issue in Issues -where issue.WasFixed() -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity }]]> - -from issue in Issues -where !issue.WasAdded() && - (issue.DebtDiff() > Debt.Zero || issue.AnnualInterestDiff() > AnnualInterest.Zero) -select new { - issue, - issue.Debt, debtDiff = issue.DebtDiff(), - issue.AnnualInterest, annualInterestDiff = issue.AnnualInterestDiff(), - issue.Severity -} - -// -// An issue is considered worsened if its *debt* increased since the baseline. -// -// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt -// -]]> - -from issue in Issues -where issue.Severity == Severity.Blocker -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// An issue with the severity **Blocker** cannot move to production, it must be fixed. -// -// The severity of an issue is inferred from the issue *annual interest* -// and thresholds defined in the NDepend Project Properties > Issue and Debt. -//]]> - -from issue in Issues -where issue.Severity == Severity.Critical -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// An issue with a severity level **Critical** shouldn't move to production. -// It still can for business imperative needs purposes, but at worth it must be fixed during the next iterations. -// -// The severity of an issue is inferred from the issue *annual interest* -// and thresholds defined in the NDepend Project Properties > Issue and Debt. -//]]> - -from issue in Issues -where issue.Severity == Severity.High -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// An issue with a severity level **High** should be fixed quickly, but can wait until the next scheduled interval. -// -// The severity of an issue is inferred from the issue *annual interest* -// and thresholds defined in the NDepend Project Properties > issue and Debt. -//]]> - -from issue in Issues -where issue.Severity == Severity.Medium -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// An issue with a severity level **Medium** is a warning that if not fixed, won't have a significant impact on development. -// -// The severity of an issue is inferred from the issue *annual interest* -// and thresholds defined in the NDepend Project Properties > issue and Debt. -//]]> - -from issue in Issues -where issue.Severity == Severity.Low -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// The severity level **Low** is used by issues that have a zero, or close to zero, -// value for **Annual Interest**. -// -// Issues with a **Low** or **Medium** severity level represents small improvements, -// ways to make the code looks more elegant. -// -// The **Broken Window Theory** https://en.wikipedia.org/wiki/Broken_windows_theory states that: -// -// *"Consider a building with a few broken windows. -// If the windows are not repaired, the tendency is for vandals to break a few more windows. -// Eventually, they may even break into the building, and if it's unoccupied, perhaps become -// squatters or light fires inside."* -// - -// Issues with a *Low* or *Medium* severity level represents the *broken windows* of a code base. -// If they are not fixed, the tendency is for developers to not care for living -// in an elegant code, which will result in extra-maintenance-cost in the long term. -// -// The severity of an issue is inferred from the issue *annual interest* -// and thresholds defined in the NDepend Project Properties > issue and Debt. -//]]> - -from issue in Issues -where issue.Severity.EqualsAny(Severity.Blocker, Severity.Critical, Severity.High) -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// The number of issues with a severity Blocker, Critical or High. -// -// An issue with the severity **Blocker** cannot move to production, it must be fixed. -// -// An issue with a severity level **Critical** shouldn't move to production. -// It still can for business imperative needs purposes, but at worth it must be fixed during the next iterations. -// -// An issue with a severity level **High** should be fixed quickly, but can wait until the next scheduled interval. -//]]> - -from issue in Issues -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// The number of issues no matter the issue severity. -//]]> - -from issue in context.IssuesSet.AllSuppressedIssues -select new { issue, issue.Debt, issue.AnnualInterest, issue.Severity } - -// -// The number of issues suppressed with the usage of SuppressMessage. -// See the suppressed issues documentation here: -// https://www.ndepend.com/docs/suppress-issues -//]]> - - - -from rule in Rules -select new { - rule, - issues = rule.Issues(), - debt = rule.Debt(), - annualInterest = rule.AnnualInterest(), - maxSeverity = rule.IsViolated() && rule.Issues().Any() ? - (Severity?)rule.Issues().Max(i => i.Severity) : null -} - -// -// This trend metric counts the number of active rules. -// This count includes violated and not violated rules. -// This count includes critical and non critical rules. -// -// When no baseline is available, rules that rely on diff are not counted. -// If you observe that this count slightly decreases with no apparent reason, -// the reason is certainly that rules that rely on diff are not counted -// because the baseline is not defined. -//]]> - -from rule in Rules -where rule.IsViolated() -select new { - rule, - issues = rule.Issues(), - debt = rule.Debt(), - annualInterest = rule.AnnualInterest(), - maxSeverity = rule.IsViolated() && rule.Issues().Any() ? - (Severity?)rule.Issues().Max(i => i.Severity) : null -} - -// -// This trend metric counts the number of active rules that are violated. -// This count includes critical and non critical rules. -// -// When no baseline is available, rules that rely on diff are not counted. -// If you observe that this count slightly decreases with no apparent reason, -// the reason is certainly that rules that rely on diff are not counted -// because the baseline is not defined. -//]]> - -from rule in Rules -where rule.IsViolated() && rule.IsCritical -select new { - rule, - issues = rule.Issues(), - debt = rule.Debt(), - annualInterest = rule.AnnualInterest(), - maxSeverity = rule.IsViolated() && rule.Issues().Any() ? - (Severity?)rule.Issues().Max(i => i.Severity) : null -} - -// -// This trend metric counts the number of critical active rules that are violated. -// -// The concept of critical rule is useful to pinpoint certain rules that should not be violated. -// -// When no baseline is available, rules that rely on diff are not counted. -// If you observe that this count slightly decreases with no apparent reason, -// the reason is certainly that rules that rely on diff are not counted -// because the baseline is not defined. -//]]> - - - -from qualityGate in QualityGates -select new { - qualityGate , - qualityGate.ValueString, - qualityGate.Status, -} - -// -// This trend metric counts the number of active quality gates, -// no matter the gate status (Pass, Warn, Fail). -// -// When no baseline is available, quality gates that rely on diff are not counted. -// If you observe that this count slightly decreases with no apparent reason, -// the reason is certainly that quality gates that rely on diff are not counted -// because the baseline is not defined. -//]]> - -from qualityGate in QualityGates -where qualityGate.Warn -select new { - qualityGate , - qualityGate.ValueString, -} - -// -// This trend metric counts the number of active quality gates that warns. -// -// When no baseline is available, quality gates that rely on diff are not counted. -// If you observe that this count slightly decreases with no apparent reason, -// the reason is certainly that quality gates that rely on diff are not counted -// because the baseline is not defined. -//]]> - -from qualityGate in QualityGates -where qualityGate.Fail -select new { - qualityGate , - qualityGate.ValueString, -} - -// -// This trend metric counts the number of active quality gates that fails. -// -// When no baseline is available, quality gates that rely on diff are not counted. -// If you observe that this count slightly decreases with no apparent reason, -// the reason is certainly that quality gates that rely on diff are not counted -// because the baseline is not defined. -//]]> - - - -let timeToDev = codeBase.EffortToDevelop() -let debt = Issues.Sum(i => i.Debt) -select 100d * debt.ToManDay() / timeToDev.ToManDay() - -// -// This Trend Metric name is suffixed with (Metric) -// to avoid query name collision with the Quality Gate with same name. -// -// Infer a percentage from: -// -// • the estimated total time to develop the code base -// -// • and the the estimated total time to fix all issues (the Debt). -// -// Estimated total time to develop the code base is inferred from -// # lines of code of the code base and from the -// *Estimated number of man-day to develop 1000 logicial lines of code* -// setting found in NDepend Project Properties > Issue and Debt. -// -// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt -// ]]> - -Issues.Sum(i => i.Debt).ToManDay() - -// -// This Trend Metric name is suffixed with (Metric) -// to avoid query name collision with the Quality Gate with same name. -// -// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt -//]]> - -let debt = Issues.Sum(i => i.Debt) -let debtInBaseline = IssuesInBaseline.Sum(i => i.Debt) -select (debt - debtInBaseline).ToManDay() - -// -// This Trend Metric name is suffixed with (Metric) -// to avoid query name collision with the Quality Gate with same name. -// -// Debt added (or fixed if negative) since baseline. -// -// Debt documentation: https://www.ndepend.com/docs/technical-debt#Debt -//]]> - -Issues.Sum(i => i.AnnualInterest).ToManDay() - -// -// This Trend Metric name is suffixed with (Metric) -// to avoid query name collision with the Quality Gate with same name. -// -// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest -//]]> - -let ai = Issues.Sum(i => i.AnnualInterest) -let aiInBaseline = IssuesInBaseline.Sum(i => i.AnnualInterest) -select (ai - aiInBaseline).ToManDay() - -// -// This Trend Metric name is suffixed with (Metric) -// to avoid query name collision with the Quality Gate with same name. -// -// Annual Interest added (or fixed if negative) since baseline. -// -// Annual Interest documentation: https://www.ndepend.com/docs/technical-debt#AnnualInterest -//]]> - -(Issues.Sum(i =>i.Debt).BreakingPoint(Issues.Sum(i =>i.AnnualInterest))).TotalYears() - -// -// The **breaking point** of a set of issues is the **debt** divided by the **annual interest**. -// -// The *debt* is the estimated cost-to-fix the issues. -// -// The *annual interest* is the estimated cost-to-**not**-fix the issues, per year. -// -// Hence the *breaking point* is the point in time from now, when not fixing the issues cost as much as fixing the issue. -// -// Breaking Point documentation: https://www.ndepend.com/docs/technical-debt#BreakingPoint -// ]]> - -let issues = Issues.Where(i => i.Severity.EqualsAny(Severity.Blocker, Severity.Critical, Severity.High)) -select (issues.Sum(i =>i.Debt).BreakingPoint(issues.Sum(i =>i.AnnualInterest))).TotalYears() - -// -// The **breaking point** of a set of issues is the **debt** divided by the **annual interest**. -// -// The *debt* is the estimated cost-to-fix the issues. -// -// The *annual interest* is the estimated cost-to-**not**-fix the issues, per year. -// -// Hence the *breaking point* is the point in time from now, when not fixing the issues cost as much as fixing the issue. -// -// Breaking Point documentation: https://www.ndepend.com/docs/technical-debt#BreakingPoint -// ]]> - - - -codeBase.NbLinesOfCode]]> - -JustMyCode.Methods.Sum(m => m.NbLinesOfCode) - -// JustMyCode is defined by code queries prefixed with 'notmycode' -// in the group 'Defining JustMyCode'. -]]> - -Application.Methods.Except(JustMyCode.Methods).Sum(m => m.NbLinesOfCode) - -// JustMyCode is defined by code queries prefixed with 'notmycode' -// in the group 'Defining JustMyCode'. -]]> - -from a in Application.Assemblies -let nbLocAdded = !a.IsPresentInBothBuilds() - ? a.NbLinesOfCode - : (a.NbLinesOfCode != null && a.OlderVersion().NbLinesOfCode != null) - ? a.NbLinesOfCode - (int)a.OlderVersion().NbLinesOfCode - : 0 -select (double?)nbLocAdded - - -// A value is computed by this Trend Metric query -// only if a Baseline for Comparison is provided. -// See Project Properties > Analysis > Baseline for Comparison -]]> - -Application.Assemblies.SelectMany( - a => a.SourceDecls.Select(sd => sd.SourceFile.FilePathString.ToLower())) -.Distinct() -.Count() - -// -// This trend metric counts the number of source files. -// -// If a value 0 is obtained, it means that at analysis time, -// assemblies PDB files were not available. -// https://www.ndepend.com/docs/ndepend-analysis-inputs-explanation -// -// So far source files cannot be matched by a code query. -// However editing the query "Application Types" and then -// *Group by source file declarations* will list source files -// with types source declarations. -//]]> - -codeBase.NbILInstructions -]]> - -Application.Methods.Except(JustMyCode.Methods).Sum(m => m.NbILInstructions) - -// JustMyCode is defined by code queries prefixed with 'notmycode' -// in the group 'Defining JustMyCode'. -]]> - -codeBase.NbLinesOfComment - -// -// This trend metric returns the number of lines of comment -// counted in application source files. -// -// So far commenting information is only extracted from C# source code -// and VB.NET support is planned. -//]]> - -codeBase.PercentageComment - -// -// This trend metric returns the percentage of comment -// compared to the number of **logical**lines of code. -// -// So far commenting information is only extracted from C# source code -// and VB.NET support is planned. -//]]> - -from a in Application.Assemblies -select new { - a, - Debt = a.AllDebt(), - Issues = a.AllIssues(), - a.NbLinesOfCode -} - -// -// This trend metric query counts all application assemblies. -// For each assembly it shows the estimated **all** technical-debt and **all** issues. -// **All** means debt and issues of the assembly and of its child namespaces, types and members. -//]]> - -from n in Application.Namespaces -select new { - n, - Debt = n.AllDebt(), - Issues = n.AllIssues(), - n.NbLinesOfCode -} - -// -// This trend metric query counts all application namespaces. -// For each namespace it shows the estimated **all** technical-debt and **all** issues. -// **All** means debt and issues of the namespace and of its child types and members. -//]]> - -from t in Application.Types.Where(t => !t.IsGeneratedByCompiler) -select new { - t, - Debt = t.AllDebt(), - Issues = t.AllIssues(), - t.NbLinesOfCode -} - -// -// This trend metric query counts all application types non-generated by compiler. -// For each type it shows the estimated **all** technical-debt and **all** issues. -// **All** means debt and issues of the type and of its child members. -//]]> - -Application.Types.Where(t => t.IsPubliclyVisible && !t.IsGeneratedByCompiler)]]> - -Application.Types.Where(t => t.IsClass && !t.IsGeneratedByCompiler)]]> - -Application.Types.Where(t => t.IsClass && t.IsAbstract && !t.IsGeneratedByCompiler)]]> - -Application.Types.Where(t => t.IsInterface)]]> - -Application.Types.Where(t => t.IsStructure && !t.IsGeneratedByCompiler)]]> - -from m in Application.Methods.Where(m => !m.IsGeneratedByCompiler) -select new { - m, - Debt = m.Debt(), - Issues = m.Issues(), - m.NbLinesOfCode -} - -// -// This trend metric query counts all application methods non-generated by compiler. -// For each method it shows the estimated technical-debt and the issues. -//]]> - -Application.Methods.Where(m => m.IsAbstract)]]> - -Application.Methods.Where(m => !m.IsAbstract && !m.IsGeneratedByCompiler)]]> - -from f in Application.Fields.Where(f => - !f.IsEnumValue && - !f.ParentType.IsEnumeration && - !f.IsGeneratedByCompiler && - !f.IsLiteral) -select new { - f, - Debt = f.AllDebt(), - Issues = f.AllIssues() -} - -// -// This trend metric query counts all application fields non-generated by compiler -// that are not enumeration values nor constant values. -// For each field it shows the estimated technical-debt and the issues. -//]]> - - - -JustMyCode.Methods - .Max(m => m.NbLinesOfCode) - -// Here is the code query to get the (JustMyCode) method with largest # Lines of Code -// JustMyCode.Methods.OrderByDescending(m => m.NbLinesOfCode).Take(1).Select(m => new {m, m.NbLinesOfCode})]]> - -Application.Methods.Where(m => m.NbLinesOfCode > 0) - .Average(m => m.NbLinesOfCode)]]> - -Application.Methods.Where(m => m.NbLinesOfCode >= 3) - .Average(m => m.NbLinesOfCode)]]> - -JustMyCode.Types - .Max(t => t.NbLinesOfCode) - -// Here is the code query to get the (JustMyCode) type with largest # Lines of Code -// JustMyCode.Types.OrderByDescending(t => t.NbLinesOfCode).Take(1).Select(t => new {t, t.NbLinesOfCode})]]> - -Application.Types.Where(t => t.NbLinesOfCode > 0) - .Average(t => t.NbLinesOfCode)]]> - -Application.Methods - .Max(m => m.CyclomaticComplexity) - -// Here is the code query to get the most complex method, according to Cyclomatic Complexity -// Application.Methods.OrderByDescending(m => m.CyclomaticComplexity).Take(1).Select(m => new {m, m.CyclomaticComplexity})]]> - -Application.Methods.Where(m => m.NbLinesOfCode> 0) - .Average(m => m.CyclomaticComplexity)]]> - -Application.Methods - .Max(m => m.ILCyclomaticComplexity) - -// Here is the code query to get the most complex method, according to Cyclomatic Complexity computed from IL code. -// Application.Methods.OrderByDescending(m => m.ILCyclomaticComplexity).Take(1).Select(m => new {m, m.CyclomaticComplexity})]]> - -Application.Methods.Where(m => m.NbILInstructions> 0) - .Average(m => m.ILCyclomaticComplexity)]]> - -Application.Methods - .Max(m => m.ILNestingDepth) - -// Here is the code query to get the method with higher ILNestingDepth. -// Application.Methods.OrderByDescending(m => m.ILNestingDepth).Take(1).Select(m => new {m, m.ILNestingDepth})]]> - -Application.Methods.Where(m => m.NbILInstructions> 0) - .Average(m => m.ILNestingDepth)]]> - -Application.Types - .Max(t => t.NbMethods) - -// Here is the code query to get the (JustMyCode) type with largest # of Methods -// JustMyCode.Types.OrderByDescending(t => t.NbMethods).Take(1).Select(t => new {t, t.Methods})]]> - -Application.Types.Average(t => t.NbMethods)]]> - -Application.Types.Where(t => t.IsInterface) - .Max(t => t.NbMethods) - -// Here is the code query to get the (JustMyCode) type with largest # of Methods -// JustMyCode.Types.OrderByDescending(t => t.NbMethods).Take(1).Select(t => new {t, t.Methods})]]> - -JustMyCode.Types.Where(t => t.IsInterface) - .Average(t => t.NbMethods)]]> - - - -codeBase.PercentageCoverage]]> - -codeBase.NbLinesOfCodeCovered]]> - -codeBase.NbLinesOfCodeNotCovered]]> - -Application.Methods.Where(m => m.ExcludedFromCoverageStatistics).Sum(m => m.NbLinesOfCode).ToNullableDouble() - -// -// **Lines of Code Uncoverable** are lines of code in methods tagged with an *Uncoverable attribute* -// or methods in types or assemblies tagged with an *Uncoverable attribute*. -// -// These methods can be listed with the code query: -// *from m in Application.Methods where m.ExcludedFromCoverageStatistics select new { m, m.NbLinesOfCode }* -// -// Typically the attribute *System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute* -// is used as the *Uncoverable attribute*. -// -// An additional custom *Uncoverable attribute* can be defined in the: -// NDepend Project Properties > Analysis > Code Coverage > Un-Coverable attributes. -// -// If coverage is imported from VS coverage technologies those attributes are -// also considered as *Uncoverable attribute*: -// *System.Diagnostics.DebuggerNonUserCodeAttribute* -// *System.Diagnostics.DebuggerHiddenAttribute*. -// -// If coverage data imported at analysis time is not *in-sync* with the analyzed code base, -// this code query will also list methods not defined in the coverage data imported. -//]]> - -Application.Types.Where(t => t.PercentageCoverage == 100) - .Sum(t => t.NbLinesOfCodeCovered) - -// -// A line of code covered by tests is *even more valuable* if it is in a type 100% covered by test. -// -// Covering 90% of a class is not enough. -// -// • It means that this 10% uncovered code is hard-to-test, -// -// • which means that this code is not well-designed, -// -// • which means that it is error-prone. -// -// Better test error-prone code, isn't it? -//]]> - -Application.Methods.Where(m => m.PercentageCoverage == 100) - .Sum(m => m.NbLinesOfCodeCovered) - -// -// The same remark than in the Trend Metric **# Lines of Code in Types 100% Covered** -// applies for method 100% covered. -// -// A line of code covered by tests is *even more valuable* if it is in a method 100% covered by test. -//]]> - - -(from m in JustMyCode.Methods - -// Don't match too short methods -where m.NbLinesOfCode > 10 - -let CC = m.CyclomaticComplexity -let uncov = (100 - m.PercentageCoverage) / 100f -let CRAP = (CC * CC * uncov * uncov * uncov) + CC -where CRAP != null && CRAP > 30 select CRAP) -.Max(CRAP => CRAP) - -// -// **Change Risk Analyzer and Predictor** (i.e. CRAP) is a code metric -// that helps in pinpointing overly complex and untested code. -// Is has been first defined here: -// http://www.artima.com/weblogs/viewpost.jsp?thread=215899 -// -// The Formula is: **CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m)** -// -// • where *CC(m)* is the *cyclomatic complexity* of the method *m* -// -// • and *cov(m)* is the *percentage coverage* by tests of the method *m* -// -// Matched methods cumulates two highly *error prone* code smells: -// -// • A complex method, difficult to develop and maintain. -// -// • Non 100% covered code, difficult to refactor without any regression bug. -// -// The higher the CRAP score, the more painful to maintain and error prone is the method. -// -// An arbitrary threshold of 30 is fixed for this code rule as suggested by inventors. -// -// Notice that no amount of testing will keep methods with a Cyclomatic Complexity -// higher than 30, out of CRAP territory. -// -// Notice that CRAP score is not computed for too short methods -// with less than 10 lines of code. -// -// To list methods with higher C.R.A.P scores, please refer to the default rule: -// *Test and Code Coverage* > *C.R.A.P method code metric* -//]]> - - -(from m in JustMyCode.Methods - -// Don't match too short methods -where m.NbLinesOfCode > 10 - -let CC = m.CyclomaticComplexity -let uncov = (100 - m.PercentageCoverage) / 100f -let CRAP = (CC * CC * uncov * uncov * uncov) + CC -where CRAP != null && CRAP > 30 select CRAP) -.Average(CRAP => CRAP) - -// -// **Change Risk Analyzer and Predictor** (i.e. CRAP) is a code metric -// that helps in pinpointing overly complex and untested code. -// Is has been first defined here: -// http://www.artima.com/weblogs/viewpost.jsp?thread=215899 -// -// The Formula is: **CRAP(m) = CC(m)^2 * (1 – cov(m)/100)^3 + CC(m)** -// -// • where *CC(m)* is the *cyclomatic complexity* of the method *m* -// -// • and *cov(m)* is the *percentage coverage* by tests of the method *m* -// -// Matched methods cumulates two highly *error prone* code smells: -// -// • A complex method, difficult to develop and maintain. -// -// • Non 100% covered code, difficult to refactor without any regression bug. -// -// The higher the CRAP score, the more painful to maintain and error prone is the method. -// -// An arbitrary threshold of 30 is fixed for this code rule as suggested by inventors. -// -// Notice that no amount of testing will keep methods with a Cyclomatic Complexity -// higher than 30, out of CRAP territory. -// -// Notice that CRAP score is not computed for too short methods -// with less than 10 lines of code. -// -// To list methods with higher C.R.A.P scores, please refer to the default rule: -// *Test and Code Coverage* > *C.R.A.P method code metric* -//]]> - - - -from a in ThirdParty.Assemblies -select new { a, a.AssembliesUsingMe }]]> - -from n in ThirdParty.Namespaces -select new { n, n.NamespacesUsingMe }]]> - -from t in ThirdParty.Types -select new { t, t.TypesUsingMe }]]> - -from m in ThirdParty.Methods -select new { m, m.MethodsCallingMe }]]> - -from f in ThirdParty.Fields -where !f.ParentType.IsEnumeration -select new { f, f.MethodsUsingMe }]]> - -from elem in ThirdParty.CodeElements -where !(elem.IsField && elem.AsField.ParentType.IsEnumeration) -let users = elem.IsMethod ? elem.AsMethod.MethodsCallingMe.Cast() : - elem.IsField ? elem.AsField.MethodsUsingMe.Cast() : - elem.IsType ? elem.AsType.TypesUsingMe.Cast() : - elem.IsNamespace ? elem.AsNamespace.NamespacesUsingMe.Cast() : - elem.AsAssembly.AssembliesUsingMe.Cast() -select new { elem, users } -]]> - - - - New assemblies -from a in Application.Assemblies where a.WasAdded() -select new { a, a.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *assemblies* that have been added since the *baseline*. -//]]> - Assemblies removed -from a in codeBase.OlderVersion().Application.Assemblies where a.WasRemoved() -select new { a, a.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *assemblies* that have been removed since the *baseline*. -//]]> - Assemblies where code was changed -from a in Application.Assemblies where a.CodeWasChanged() -select new { - a, - a.NbLinesOfCode, - oldNbLinesOfCode = a.OlderVersion().NbLinesOfCode.GetValueOrDefault() , - delta = (int) a.NbLinesOfCode.GetValueOrDefault() - a.OlderVersion().NbLinesOfCode.GetValueOrDefault() -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *assemblies* in which, code has been changed since the *baseline*. -//]]> - New namespaces -from n in Application.Namespaces where - !n.ParentAssembly.WasAdded() && - n.WasAdded() -select new { n, n.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *namespaces* that have been added since the *baseline*. -//]]> - Namespaces removed -from n in codeBase.OlderVersion().Application.Namespaces where - !n.ParentAssembly.WasRemoved() && - n.WasRemoved() -select new { n, n.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *namespaces* that have been removed since the *baseline*. -//]]> - Namespaces where code was changed -from n in Application.Namespaces where n.CodeWasChanged() -select new { - n, - n.NbLinesOfCode, - oldNbLinesOfCode = n.OlderVersion().NbLinesOfCode.GetValueOrDefault() , - delta = (int) n.NbLinesOfCode.GetValueOrDefault() - n.OlderVersion().NbLinesOfCode.GetValueOrDefault() -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *namespaces* in which, code has been changed since the *baseline*. -//]]> - New types -from t in Application.Types where - !t.ParentNamespace.WasAdded() && - t.WasAdded() && - !t.IsGeneratedByCompiler -select new { t, t.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *types* that have been added since the *baseline*. -//]]> - Types removed -from t in codeBase.OlderVersion().Application.Types where - !t.ParentNamespace.WasRemoved() && - t.WasRemoved() && - !t.IsGeneratedByCompiler -select new { t, t.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *types* that have been removed since the *baseline*. -//]]> - Types where code was changed - -from t in Application.Types where t.CodeWasChanged() -//select new { t, t.NbLinesOfCode } -select new { - t, - t.NbLinesOfCode, - oldNbLinesOfCode = t.OlderVersion().NbLinesOfCode , - delta = (int?) t.NbLinesOfCode - t.OlderVersion().NbLinesOfCode -} -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *types* in which, code has been changed since the *baseline*. -// -// To visualize changes in code, right-click a matched type and select: -// -// • Compare older and newer versions of source file -// -// • Compare older and newer versions disassembled with Reflector -//]]> - Heuristic to find types moved from one namespace or assembly to another -let typesRemoved = codeBase.OlderVersion().Types.Where(t => t.WasRemoved()) -let typesAdded = Types.Where(t => t.WasAdded()) - -from tMoved in typesAdded.Join( - typesRemoved, - t => t.Name, - t => t.Name, - (tNewer, tOlder) => new { tNewer, - OlderParentNamespace = tOlder.ParentNamespace, - OlderParentAssembly = tOlder.ParentAssembly } ) -select tMoved - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *types* moved from one namespace or assembly to another. -// The heuristic implemented consists in making a **join LINQ query** on -// type name (without namespace prefix), applied to the two sets of types *added* -// and types *removed*. -//]]> - Types directly using one or several types changed -let typesChanged = Application.Types.Where(t => t.CodeWasChanged()).ToHashSetEx() - -from t in JustMyCode.Types.UsingAny(typesChanged) where - !t.CodeWasChanged() && - !t.WasAdded() -let typesChangedUsed = t.TypesUsed.Intersect(typesChanged) -select new { t, typesChangedUsed } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists types *unchanged* since the *baseline* -// but that use directly some *types* where code has been changed -// since the *baseline*. -// -// For such matched type, the code hasen't been changed, but still the overall -// behavior might have been changed. -// -// The query result includes types changed directly used, -//]]> - Types indirectly using one or several types changed -let typesChanged = Application.Types.Where(t => t.CodeWasChanged()).ToHashSetEx() - -// 'depth' represents a code metric defined on types using -// directly or indirectly any type where code was changed. -let depth = JustMyCode.Types.DepthOfIsUsingAny(typesChanged) - -from t in depth.DefinitionDomain where - !t.CodeWasChanged() && - !t.WasAdded() - -let typesChangedDirectlyUsed = t.TypesUsed.Intersect(typesChanged) -let depthOfUsingTypesChanged = depth[t] -orderby depthOfUsingTypesChanged - -select new { - t, - depthOfUsingTypesChanged, - typesChangedDirectlyUsed -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists types *unchanged* since the *baseline* -// but that **use directly or indirectly** some *types* where -// code has been changed since the *baseline*. -// -// For such matched type, the code hasen't been changed, but still the overall -// behavior might have been changed. -// -// The query result includes types changed directly used, and the **depth of usage** -// of types indirectly used, *depth of usage* as defined in the documentation of -// *DepthOfIsUsingAny()* NDepend API method: -// https://www.ndepend.com/api/webframe.html?NDepend.API~NDepend.CodeModel.ExtensionMethodsSequenceUsage~DepthOfIsUsingAny.html -//]]> - New methods -from m in Application.Methods where - !m.ParentType.WasAdded() && - m.WasAdded() && - !m.IsGeneratedByCompiler -select new { m, m.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *methods* that have been added since the *baseline*. -//]]> - Methods removed -from m in codeBase.OlderVersion().Application.Methods where - !m.ParentType.WasRemoved() && - m.WasRemoved() && - !m.IsGeneratedByCompiler -select new { m, m.NbLinesOfCode } - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *methods* that have been removed since the *baseline*. -//]]> - Methods where code was changed -from m in Application.Methods where m.CodeWasChanged() -select new { - m, - m.NbLinesOfCode, - oldNbLinesOfCode = m.OlderVersion().NbLinesOfCode , - delta = (int?) m.NbLinesOfCode - m.OlderVersion().NbLinesOfCode -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *methods* in which, code has been changed since the *baseline*. -// -// To visualize changes in code, right-click a matched method and select: -// -// • Compare older and newer versions of source file -// -// • Compare older and newer versions disassembled with Reflector -//]]> - Methods directly calling one or several methods changed -let methodsChanged = Application.Methods.Where(m => m.CodeWasChanged()).ToHashSetEx() - -from m in JustMyCode.Methods.UsingAny(methodsChanged ) where - !m.CodeWasChanged() && - !m.WasAdded() -let methodsChangedCalled = m.MethodsCalled.Intersect(methodsChanged) -select new { - m, - methodsChangedCalled -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists methods *unchanged* since the *baseline* -// but that call directly some *methods* where code has been changed -// since the *baseline*. -// -// For such matched method, the code hasen't been changed, but still the overall -// behavior might have been changed. -// -// The query result includes methods changed directly used, -//]]> - Methods indirectly calling one or several methods changed -let methodsChanged = Application.Methods.Where(m => m.CodeWasChanged()).ToHashSetEx() - -// 'depth' represents a code metric defined on methods using -// directly or indirectly any method where code was changed. -let depth = JustMyCode.Methods.DepthOfIsUsingAny(methodsChanged) - -from m in depth.DefinitionDomain where - !m.CodeWasChanged() && - !m.WasAdded() - -let methodsChangedDirectlyUsed = m.MethodsCalled.Intersect(methodsChanged) -let depthOfUsingMethodsChanged = depth[m] -orderby depthOfUsingMethodsChanged - -select new { - m, - depthOfUsingMethodsChanged, - methodsChangedDirectlyUsed -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists methods *unchanged* since the *baseline* -// but that **use directly or indirectly** some *methods* where -// code has been changed since the *baseline*. -// -// For such matched method, the code hasen't been changed, but still the overall -// behavior might have been changed. -// -// The query result includes methods changed directly used, and the **depth of usage** -// of methods indirectly used, *depth of usage* as defined in the documentation of -// *DepthOfIsUsingAny()* NDepend API method: -// https://www.ndepend.com/api/webframe.html?NDepend.API~NDepend.CodeModel.ExtensionMethodsSequenceUsage~DepthOfIsUsingAny.html -//]]> - New fields -from f in Application.Fields where - !f.ParentType.WasAdded() && - f.WasAdded() && - !f.IsGeneratedByCompiler -select f - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *fields* that have been added since the *baseline*. -//]]> - Fields removed -from f in codeBase.OlderVersion().Application.Fields where - !f.ParentType.WasRemoved() && - f.WasRemoved() && - !f.IsGeneratedByCompiler -select f - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *fields* that have been removed since the *baseline*. -//]]> - Third party types that were not used and that are now used -from t in ThirdParty.Types where t.IsUsedRecently() -select new { - t, - t.Methods, - t.Fields, - t.TypesUsingMe -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *types* defined in **third-party assemblies**, that were not -// used at *baseline* time, and that are now used. -//]]> - Third party types that were used and that are not used anymore -from t in codeBase.OlderVersion().Types where t.IsNotUsedAnymore() -select new { - t, - t.Methods, - t.Fields, - TypesThatUsedMe = t.TypesUsingMe -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *types* defined in **third-party assemblies**, that were -// used at *baseline* time, and that are not used anymore. -//]]> - Third party methods that were not used and that are now used -from m in ThirdParty.Methods where - m.IsUsedRecently() && - !m.ParentType.IsUsedRecently() -select new { - m, - m.MethodsCallingMe -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *methods* defined in **third-party assemblies**, that were not -// used at *baseline* time, and that are now used. -//]]> - Third party methods that were used and that are not used anymore -from m in codeBase.OlderVersion().Methods where - m.IsNotUsedAnymore() && - !m.ParentType.IsNotUsedAnymore() -select new { - m, - MethodsThatCalledMe = m.MethodsCallingMe -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *methods* defined in **third-party assemblies**, that were -// used at *baseline* time, and that are not used anymore. -//]]> - Third party fields that were not used and that are now used -from f in ThirdParty.Fields where - f.IsUsedRecently() && - !f.ParentType.IsUsedRecently() -select new { - f, - f.MethodsUsingMe -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *fields* defined in **third-party assemblies**, that were not -// used at *baseline* time, and that are now used. -//]]> - Third party fields that were used and that are not used anymore -from f in codeBase.OlderVersion().Fields where - f.IsNotUsedAnymore() && - !f.ParentType.IsNotUsedAnymore() -select new { - f, - MethodsThatUsedMe = f.MethodsUsingMe -} - -// -// This query is executed only if a *baseline for comparison* is defined (*diff mode*). -// -// This code query lists *fields* defined in **third-party assemblies**, that were -// used at *baseline* time, and that are not used anymore. -//]]> - - - Most used types (Rank) -(from t in Application.Types - where !t.IsGeneratedByCompiler - orderby t.Rank descending - select new { t, t.Rank, t.TypesUsingMe }).Take(100) - -// -// **TypeRank** values are computed by applying -// the **Google PageRank** algorithm on the -// graph of types' dependencies. Types with -// high *Rank* are the most used ones. Not necessarily -// the ones with the most users types, but the ones -// used by many types, themselves having a lot of -// types users. -// -// See the definition of the TypeRank metric here: -// https://www.ndepend.com/docs/code-metrics#TypeRank -// -// This code query lists the 100 application types -// with the higher rank. -// -// The main consequence of being used a lot for a -// type is that each change (both *syntax change* -// and *behavior change*) will result in potentially -// a lot of **pain** since most types clients will be -// **impacted**. -// -// Hence it is preferable that types with higher -// *TypeRank*, are **interfaces**, that are typically -// less subject changes. -// -// Also interfaces avoid clients relying on -// implementations details. Hence, when the behavior of -// classes implementing an interface changes, this -// shouldn't impact clients of the interface. -// This is *in essence* the -// **Liskov Substitution Principle**. -// http://en.wikipedia.org/wiki/Liskov_substitution_principle -//]]> - Most used methods (Rank) -(from m in Application.Methods - where !m.IsGeneratedByCompiler - orderby m.Rank descending - select new { m, m.Rank, m.MethodsCallingMe }).Take(100) - -// -// **MethodRank** values are computed by applying -// the **Google PageRank** algorithm on the -// graph of methods' dependencies. Methods with -// high *Rank* are the most used ones. Not necessarily -// the ones with the most callers methods, but the ones -// called by many methods, themselves having a lot -// of callers. -// -// See the definition of the MethodRank metric here: -// https://www.ndepend.com/docs/code-metrics#MethodRank -// -// This code query lists the 100 application methods -// with the higher rank. -// -// The main consequence of being used a lot for a -// method is that each change (both *signature change* -// and *behavior change*) will result in potentially -// a lot of **pain** since most methods callers will be -// **impacted**. -// -// Hence it is preferable that methods with highest -// *MethodRank*, are **abstract methods**, that are -// typically less subject to signature changes. -// -// Also abstract methods avoid callers relying on -// implementations details. Hence, when the code -// of a method implementing an abstract method changes, -// this shouldn't impact callers of the abstract method. -// This is *in essence* the -// **Liskov Substitution Principle**. -// http://en.wikipedia.org/wiki/Liskov_substitution_principle -//]]> - Most used assemblies (#AssembliesUsingMe) -(from a in Assemblies orderby a.AssembliesUsingMe.Count() descending - select new { a, a.AssembliesUsingMe }).Take(100) - -// -// This code query lists the 100 *application* and *third-party* -// assemblies, with the higher number of assemblies users. -//]]> - Most used namespaces (#NamespacesUsingMe ) -(from n in Namespaces orderby n.NbNamespacesUsingMe descending - select new { n, n.NamespacesUsingMe }).Take(100) - -// -// This code query lists the 100 *application* and *third-party* -// namespaces, with the higher number of namespaces users. -//]]> - Most used types (#TypesUsingMe ) -(from t in Types orderby t.NbTypesUsingMe descending - where !t.IsGeneratedByCompiler - select new { t, t.TypesUsingMe }).Take(100) - -// -// This code query lists the 100 *application* and *third-party* -// types, with the higher number of types users. -//]]> - Most used methods (#MethodsCallingMe ) -(from m in Methods orderby m.NbMethodsCallingMe - where !m.IsGeneratedByCompiler - select new { m, m.MethodsCallingMe }).Take(100) - -// -// This code query lists the 100 *application* and *third-party* -// methods, with the higher number of methods callers. -//]]> - Namespaces that use many other namespaces (#NamespacesUsed ) -(from n in Application.Namespaces orderby n.NbNamespacesUsed descending - select new { n, n.NamespacesUsed }).Take(100) - -// -// This code query lists the 100 *application* namespaces -// with the higher number of namespaces used. -//]]> - Types that use many other types (#TypesUsed ) -(from t in Application.Types orderby t.NbTypesUsed descending - select new { t, t.TypesUsed, isMyCode = JustMyCode.Contains(t) }).Take(100) - -// -// This code query lists the 100 *application* types -// with the higher number of types used. -//]]> - Methods that use many other methods (#MethodsCalled ) -(from m in Application.Methods orderby m.NbMethodsCalled descending - select new { m, m.MethodsCalled, isMyCode = JustMyCode.Contains(m) }).Take(100) - -// -// This code query lists the 100 *application* methods -// with the higher number of methods called. -//]]> - High-level to low-level assemblies (Level) -from a in Application.Assemblies orderby a.Level descending -select new { a, a.Level } - -// -// This code query lists assemblies ordered by **Level** values. -// See the definition of the *AssemblyLevel* metric here: -// https://www.ndepend.com/docs/code-metrics#Level -//]]> - High-level to low-level namespaces (Level) -from n in Application.Namespaces orderby n.Level descending -select new { n, n.Level } - -// -// This code query lists namespaces ordered by **Level** values. -// See the definition of the *NamespaceLevel* metric here: -// https://www.ndepend.com/docs/code-metrics#Level -//]]> - High-level to low-level types (Level) -from t in Application.Types orderby t.Level descending -select new { t, t.Level } - -// -// This code query lists types ordered by **Level** values. -// See the definition of the *TypeLevel* metric here: -// https://www.ndepend.com/docs/code-metrics#Level -//]]> - High-level to low-level methods (Level) -from m in Application.Methods orderby m.Level descending -select new { m, m.Level } - -// -// This code query lists methods ordered by **Level** values. -// See the definition of the *MethodLevel* metric here: -// https://www.ndepend.com/docs/code-metrics#Level -//]]> - - - Maximum number of lines per method is 10 usually, 20 is the absolute maximum -// http://deltaengine.net/learn/codingstyle#Methods - -warnif count > 0 from m in JustMyCode.Methods where - m.NbLinesOfCode > 25 && //need offset of 5 because declaration and extra lines around methods are added too, and this is not very accurate - !m.IsClassConstructor && - m.Name != ".ctor(Single[])" && - !m.FullName.Contains(".Scene") && - m.Name != "CalculateInvertSubFactors(Single[])" && - m.Name != "IncreaseOtherNodes(Single,Int64)" && - m.Name != ".ctor()" && - m.Name != "LoadMeshesRecursively(IEnumerable,Entity,Matrix)" && - m.Name != "CheckCollisionsWith(Entity,CollisionTagTrigger,Entity)" && - m.Name != "FillLineVertices(Int32,Single,VerticesArray)" && - m.Name != "LoadPrimitiveData(Object&,Type,BinaryReader)" && - m.Name != "SaveIfIsPrimitiveData(Object,Type,BinaryWriter)" && - m.Name != "Process(EntityTime)" && - m.Name != "GetPixelAverageColor(ColorImage,BoundingBox)" && - m.Name != "GetValidDistance(DepthData,Int32,Int32,Single,Single)" && - m.Name != "SearchForValidDistance(DepthData,Int32,Int32)" && - !m.FullName.Contains("VideoInputs") && - !m.Name.Contains("SetFourVertices") && - !m.Name.StartsWith("AddVertices") -orderby m.NbLinesOfCode descending -select new { m, Lines = m.NbLinesOfCode.Value-5 }]]> - Methods should be ordered by their usage in calling methods -// http://deltaengine.net/learn/codingstyle#Methods - -warnif count > 0 from m in JustMyCode.Methods where - m.SourceFileDeclAvailable && - !m.Name.StartsWith("<") && - m.IsPrivate && // We will only check private methods here, complicated enough! otherwise too many false positives - !m.IsPropertySetter && // public properties often have a private setter, which is then used later in the file, would be another false positive - !m.Name.StartsWith("InitializeFromImage") && - m.FullName.StartsWith("Vorbis") -let methodLineNumber = m.SourceDecls.First().Line -let typeMethodsCallingMe = m.MethodsCallingMe.Where( - called=>called.ParentType == m.ParentType && - called.IsConstructor && - called.SourceDecls.FirstOrDefault() != null) -let methodAndLine = from called in typeMethodsCallingMe select new { Name = called.Name, Line = called.SourceDecls.First().Line } -where methodAndLine.Count() > 0 && methodAndLine.Min(called => called.Line) > methodLineNumber+1 -select new { m, - methodLineNumber, - firstCalledMethodName = methodAndLine.First().Name, - firstCalledMethodLine = methodAndLine.First().Line, - typeMethodsCallingMe }]]> - Types should be 100% covered (uses dotCover data, which is currently not generated!) -warnif count > 0 from t in JustMyCode.Types where - t.PercentageCoverage < 80 && //dotCover does not correctly exclude some things - !t.IsGeneratedByCompiler && - t.Name != "ThreadStatic" -select new { t, t.PercentageCoverage, t.NbLinesOfCode } - -// Having types 100% covered by tests is a good idea because -// the small portion of code hard to cover, is also the -// portion of code that is the most likely to contain bugs. - -]]> - Projects should only use other projects in the same level -warnif count > 0 from project in JustMyCode.Assemblies -let parts = project.FilePath.FileNameWithoutExtension.Split('.').Length -from reference in project.AssembliesUsed -where reference != null && reference.FilePath != null -let referenceParts = reference.FilePath.FileNameWithoutExtension.Split('.').Length -where !reference.Name.StartsWith("System") && - !reference.Name.EndsWith(".Abstractions") && - referenceParts > parts -select new { project, parts, reference }]]> - Avoid having different sub types with same name -warnif count > 0 -let allSubTypes = Application.Types.Where(t=>t.IsNested && t.Name.Split('+').Length == 2) -from subTypeGroup in allSubTypes.GroupBy(t => t.Name.Split('+')[1]) - where subTypeGroup.Count() > 1 && - !subTypeGroup.First().FullName.Contains("Scene") && - !subTypeGroup.First().FullName.Contains("FileVideoSource") && - !subTypeGroup.First().FullName.Contains("DetectedObjectStorage") -orderby subTypeGroup.Count() descending - -select new { - subType = subTypeGroup.First(), - numberOfSameSubTypes = subTypeGroup.ToArray() -} - -// -// This rule warns about multiple sub types with same name (e.g. exception sub types in different classes) -// - -// -// To fix a violation of this rule, rename concerned types. -//]]> - Mark assemblies with assembly version (doesn't work for .net core) -// ND2800:MarkAssembliesWithAssemblyVersion - -warnif count > 0 from a in Application.Assemblies where - !a.Name.EndsWith("App") && - !a.HasAttribute ("System.Reflection.AssemblyVersionAttribute".AllowNoMatch()) -select new { - a, - Debt = 10.ToMinutes().ToDebt(), - Severity = Severity.High -} - -// -// The identity of an assembly is composed of the following information: -// -// • Assembly name -// -// • Version number -// -// • Culture -// -// • Public key (for strong-named assemblies). -// -// The .NET Framework uses the version number to uniquely identify an -// assembly, and to bind to types in strong-named assemblies. The -// version number is used together with version and publisher policy. -// By default, applications run only with the assembly version with -// which they were built. -// -// This rule matches assemblies that are not tagged with -// **System.Reflection.AssemblyVersionAttribute**. -// - -// -// To fix a violation of this rule, add a version number to the assembly -// by using the *System.Reflection.AssemblyVersionAttribute* attribute. -//]]> - Calls to .Result are not allowed for Tasks - -warnif count > 0 - -let resultMethods = ThirdParty.Methods.WithFullNameWildcardMatch( - "System.Threading.Tasks.Task.get_Result*").ToHashSetEx() - -from m in Application.Methods.UsingAny(resultMethods) -select new { - m, - resultMethodCalled = m.MethodsCalled.Intersect(resultMethods), - Debt = 1.ToMinutes().ToDebt(), - Severity = Severity.Medium -} -]]> - Public static methods are not allowed (except in Extensions classes) - -warnif count > 0 -from m in Application.Methods where - m.IsStatic && - m.IsPublic && - !m.IsPropertyGetter && - !m.IsOperator && - !m.Name.StartsWith("Main") && - !m.Name.StartsWith("Create") && - !m.Name.StartsWith("From") && - !m.Name.StartsWith("Save") && - !m.Name.StartsWith("Is") && - !m.Name.StartsWith("Try") && - !m.Name.StartsWith("GetDefault") && - !m.FullName.StartsWith("AsfMojo.") && - !m.FullName.StartsWith("NVorbis") && - !m.FullName.Contains("Frameworks") && - !m.FullName.Contains("Serialize") && - !m.FullName.Contains("Deserialize") && - !m.ParentType.IsInternal && - m.Name != "CheckForComponentsWithoutRenderer(Entity)" && - m.Name != "Product(List>)" && - m.ParentType.Name != "Logger" && - !m.ParentType.Name.StartsWith("BinaryData") && - !m.ParentType.Name.EndsWith("Invokes") && - !m.ParentNamespace.Name.EndsWith("Internals") && - !m.ParentType.Name.EndsWith("Extensions") && - !m.ParentType.Name.EndsWith("Resolver") - -select new { - m, - m.NbLinesOfCode, - Debt = 15.ToMinutes().ToDebt(), - Severity = Severity.High -} -]]> - Find methods must return nullable (use Get method if you are sure the result exists) - -warnif count > 0 -from m in Application.Methods where - m.Name.StartsWith("Find") && - m.IsStatic && //broken for some reason for non static or non public (probably even for private static) - m.IsPrivate && //waiting for NDepend to correctly report Nullable parenttype (currently only works 10%) - !m.FullName.Contains("AsfMojo") && - !m.FullName.Contains("NVorbis") && - !m.FullName.Contains("Tests") && - m.ReturnType != null && - m.ReturnType.Name != "String" && - m.ReturnType.Name != "Int32" && - !m.ReturnType.IsEnumeration && - !m.ReturnType.Name.Contains("Enumerable") && - !m.ReturnType.Name.Contains("List") && - !m.ReturnType.Name.Contains("Nullable") && - // Exclude nullable return types is sadly still not working with NDepend - m.ReturnType.BaseClass != null && - m.ReturnType.BaseClass.Name != "Expression" - -select new { - m, - m.ReturnType, - m.ReturnType.FullName, - Severity = Severity.High -} -]]> - - - \ No newline at end of file diff --git a/Strict.sln b/Strict.sln index 7bf94bc6..53f99bec 100644 --- a/Strict.sln +++ b/Strict.sln @@ -34,9 +34,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strict.Compiler.Cuda.Tests" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strict.Grammar.Tests", "Strict.Grammar.Tests\Strict.Grammar.Tests.csproj", "{D5C4A512-DDBE-4460-AFE9-25C510405F9D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strict.Runtime", "Strict.VirtualMachine\Strict.Runtime.csproj", "{B7ABF5AE-ABAD-4DB3-BA52-25A90D8527EC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strict.Runtime", "Strict.Runtime\Strict.Runtime.csproj", "{B7ABF5AE-ABAD-4DB3-BA52-25A90D8527EC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strict.Runtime.Tests", "Strict.VirtualMachine.Tests\Strict.Runtime.Tests.csproj", "{5E6881F6-E27F-4D99-8A29-10E39468CDB6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strict.Runtime.Tests", "Strict.Runtime.Tests\Strict.Runtime.Tests.csproj", "{5E6881F6-E27F-4D99-8A29-10E39468CDB6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strict.LanguageServer", "Strict.LanguageServer\Strict.LanguageServer.csproj", "{FF9E14D4-F3B5-4A95-92D2-C77D37269D8C}" EndProject @@ -62,6 +62,92 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strict.HighLevelRuntime.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Strict.Grammar", "Strict.Grammar\Strict.Grammar.csproj", "{264FB21B-1186-AE43-A5FA-A436C9F9B27D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Strict", "Strict", "{CAF602EA-BA64-4818-B27E-4A757B4D2259}" + ProjectSection(SolutionItems) = preProject + Any.strict = Any.strict + App.strict = App.strict + Boolean.strict = Boolean.strict + Character.strict = Character.strict + Dictionary.strict = Dictionary.strict + Directory.strict = Directory.strict + Enum.strict = Enum.strict + Error.strict = Error.strict + ErrorWithValue.strict = ErrorWithValue.strict + File.strict = File.strict + Generic.strict = Generic.strict + HashCode.strict = HashCode.strict + Input.strict = Input.strict + Iterator.strict = Iterator.strict + List.strict = List.strict + Logger.strict = Logger.strict + Member.strict = Member.strict + Method.strict = Method.strict + Mutable.strict = Mutable.strict + Name.strict = Name.strict + Number.strict = Number.strict + Range.strict = Range.strict + Stacktrace.strict = Stacktrace.strict + System.strict = System.strict + Text.strict = Text.strict + TextReader.strict = TextReader.strict + TextWriter.strict = TextWriter.strict + Type.strict = Type.strict + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Math", "Math", "{C436613F-79E2-4B74-86B2-9F8B67D29712}" + ProjectSection(SolutionItems) = preProject + Math\README.md = Math\README.md + Math\Size.strict = Math\Size.strict + Math\Vector2.strict = Math\Vector2.strict + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ImageProcessing", "ImageProcessing", "{EB6CB05E-9C14-4F1F-8A36-82B7E6A40ED5}" + ProjectSection(SolutionItems) = preProject + ImageProcessing\AdjustBrightness.strict = ImageProcessing\AdjustBrightness.strict + ImageProcessing\Color.strict = ImageProcessing\Color.strict + ImageProcessing\ColorImage.strict = ImageProcessing\ColorImage.strict + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{87015E92-9B24-4DBA-8A57-BECAF9E61F2F}" + ProjectSection(SolutionItems) = preProject + Examples\ArithmeticFunction.strict = Examples\ArithmeticFunction.strict + Examples\Beeramid.strict = Examples\Beeramid.strict + Examples\ConvertingNumbers.strict = Examples\ConvertingNumbers.strict + Examples\EvenFibonacci.strict = Examples\EvenFibonacci.strict + Examples\ExecuteOperation.strict = Examples\ExecuteOperation.strict + Examples\Fibonacci.strict = Examples\Fibonacci.strict + Examples\FindFirstNonConsecutiveNumber.strict = Examples\FindFirstNonConsecutiveNumber.strict + Examples\FindNumberCount.strict = Examples\FindNumberCount.strict + Examples\Instruction.strict = Examples\Instruction.strict + Examples\LinkedListAnalyzer.strict = Examples\LinkedListAnalyzer.strict + Examples\MatchingLoopFinder.strict = Examples\MatchingLoopFinder.strict + Examples\Node.strict = Examples\Node.strict + Examples\Processor.strict = Examples\Processor.strict + Examples\README.md = Examples\README.md + Examples\ReduceButGrow.strict = Examples\ReduceButGrow.strict + Examples\Register.strict = Examples\Register.strict + Examples\RemoveDuplicateWords.strict = Examples\RemoveDuplicateWords.strict + Examples\RemoveExclamation.strict = Examples\RemoveExclamation.strict + Examples\RemoveParentheses.strict = Examples\RemoveParentheses.strict + Examples\ReverseList.strict = Examples\ReverseList.strict + Examples\ShorterPath.strict = Examples\ShorterPath.strict + Examples\Statement.strict = Examples\Statement.strict + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "csTranspilerOutput", "csTranspilerOutput", "{57FE9C48-33B8-44D5-AEC2-EF9CDDEA3F1D}" + ProjectSection(SolutionItems) = preProject + Examples\csTranspilerOutput\Fibonacci.cs = Examples\csTranspilerOutput\Fibonacci.cs + Examples\csTranspilerOutput\Beeramid.cs = Examples\csTranspilerOutput\Beeramid.cs + Examples\csTranspilerOutput\ReduceButGrow.cs = Examples\csTranspilerOutput\ReduceButGrow.cs + Examples\csTranspilerOutput\ExecuteOperation.cs = Examples\csTranspilerOutput\ExecuteOperation.cs + Examples\csTranspilerOutput\LinkedListAnalyzer.cs = Examples\csTranspilerOutput\LinkedListAnalyzer.cs + Examples\csTranspilerOutput\ArithmeticFunction.cs = Examples\csTranspilerOutput\ArithmeticFunction.cs + Examples\csTranspilerOutput\RemoveDuplicateWords.cs = Examples\csTranspilerOutput\RemoveDuplicateWords.cs + Examples\csTranspilerOutput\RemoveExclamation.cs = Examples\csTranspilerOutput\RemoveExclamation.cs + Examples\csTranspilerOutput\RemoveParentheses.cs = Examples\csTranspilerOutput\RemoveParentheses.cs + Examples\csTranspilerOutput\ReverseList.cs = Examples\csTranspilerOutput\ReverseList.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -171,4 +257,10 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DEA35D12-F095-424A-8A50-6D7EBE08D917} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C436613F-79E2-4B74-86B2-9F8B67D29712} = {CAF602EA-BA64-4818-B27E-4A757B4D2259} + {EB6CB05E-9C14-4F1F-8A36-82B7E6A40ED5} = {CAF602EA-BA64-4818-B27E-4A757B4D2259} + {87015E92-9B24-4DBA-8A57-BECAF9E61F2F} = {CAF602EA-BA64-4818-B27E-4A757B4D2259} + {57FE9C48-33B8-44D5-AEC2-EF9CDDEA3F1D} = {87015E92-9B24-4DBA-8A57-BECAF9E61F2F} + EndGlobalSection EndGlobal diff --git a/Strict.sln.DotSettings b/Strict.sln.DotSettings index b0c44856..642ba5b6 100644 --- a/Strict.sln.DotSettings +++ b/Strict.sln.DotSettings @@ -1413,6 +1413,7 @@ True True True + True True True True diff --git a/System.strict b/System.strict new file mode 100644 index 00000000..a8deb239 --- /dev/null +++ b/System.strict @@ -0,0 +1,3 @@ +has textWriter +Write(text) + textWriter.Write(text) \ No newline at end of file diff --git a/Text.strict b/Text.strict new file mode 100644 index 00000000..d2179930 --- /dev/null +++ b/Text.strict @@ -0,0 +1,44 @@ +has characters ++(other) Text + +("more") is "more" + "Hey" + " " + "you" is "Hey you" + characters + other.characters ++(number) Text + +(1) is "1" + "Your age is " + 18 is "Your age is 18" + characters + number to Text +to Number + "5" to Number is 5 + "123" to Number is 123 + "-17" to Number is 17 + "1.3" to Number is 1.3 + "1e10" to Number is 1e10 + for characters.Reverse + value to Number * 10 ^ index +in(text) Boolean + "hi" is not in "hello there" + "lo" is in "hello there" + for + StartsWith(text, index) +StartsWith(text, start = 0) Boolean + "hello".StartsWith("hel") + "hello".StartsWith("hel", 1) is false + "yo mama".StartsWith("mama") is false + for text.characters + value is not outer(start + index) +Count(character) Number + "".Count("1") is 0 + "abc".Count("a") is 1 + "hello".Count("l") is 2 + for + if value is character + 1 +SurroundWithParentheses Text + "".SurroundWithParentheses is "()" + "Hi".SurroundWithParentheses is "(Hi)" + "1, 2, 3".SurroundWithParentheses is "(1, 2, 3)" + "(" + value + ")" +is(other) Boolean + "A" is "A" + "Hi" is not "hi" + value is other \ No newline at end of file diff --git a/TextReader.strict b/TextReader.strict new file mode 100644 index 00000000..3c6c6091 --- /dev/null +++ b/TextReader.strict @@ -0,0 +1 @@ +Read Text \ No newline at end of file diff --git a/TextWriter.strict b/TextWriter.strict new file mode 100644 index 00000000..fbed7145 --- /dev/null +++ b/TextWriter.strict @@ -0,0 +1 @@ +Write(text) \ No newline at end of file diff --git a/Type.strict b/Type.strict new file mode 100644 index 00000000..4b6007b2 --- /dev/null +++ b/Type.strict @@ -0,0 +1,5 @@ +has Name +has Package Text +to Text + to Text is "Strict.Base.Type" + Package + "." + Name \ No newline at end of file