#!/usr/bin/env python3
"""Tests for lint_csharp.
Run with: python -m unittest test_lint_csharp (from this directory)
"""
import json
import os
import tempfile
import unittest
import lint_csharp as lint
def padded(prefix, length):
"""Returns prefix followed by words of filler, exactly length columns long."""
text = (prefix + " word" * length)[:length]
if text.endswith(" "):
text = text[:-1] + "x"
return text
def rules(text, touched=None):
return [issue.rule for issue in lint.lint(text, touched)]
class FillTests(unittest.TestCase):
def test_flags_a_line_ending_where_the_next_word_still_fits(self):
source = "// The first line stops short.\n// next line continues the sentence.\n"
self.assertEqual(["comment-fill"], rules(source))
def test_reports_the_column_and_the_word_that_fits(self):
issue = lint.lint("// Short.\n// Then more.\n")[0]
self.assertEqual(1, issue.line)
self.assertIn("column 9", issue.message)
self.assertIn('"Then"', issue.message)
def test_passes_a_line_the_next_word_would_overflow(self):
first = padded("// Filled", 115)
source = first + "\n// overflowing continues.\n"
self.assertEqual([], rules(source))
def test_flags_a_word_that_lands_exactly_on_the_limit(self):
first = padded("// Filled", 114)
source = first + "\n// abcde continues.\n"
self.assertEqual(["comment-fill"], rules(source))
def test_passes_a_blank_comment_line_between_paragraphs(self):
self.assertEqual([], rules("// One paragraph.\n//\n// Another paragraph.\n"))
def test_passes_a_line_before_a_list_item(self):
self.assertEqual([], rules("// Some things.\n// - a thing\n// - another thing\n"))
def test_checks_a_list_item_continued_under_its_text(self):
source = "// - a short item\n// continued here.\n"
self.assertEqual(["comment-fill"], rules(source))
def test_passes_a_line_before_a_todo(self):
self.assertEqual([], rules("// Does the thing.\n// TODO: do it faster.\n"))
def test_passes_a_line_ending_in_a_colon(self):
self.assertEqual([], rules("// The result is:\n// the answer itself.\n"))
def test_passes_a_formula(self):
self.assertEqual([], rules("// Solve it.\n// x = (a * b) / -c\n"))
def test_passes_commented_out_code(self):
self.assertEqual([], rules("// counter = 0;\n// counter++;\n"))
def test_passes_doc_tags_that_open_or_close_a_block(self):
source = ("/// \n/// Returns the thing.\n/// \n"
"/// The a.\n/// The b.\n")
self.assertEqual([], rules(source))
def test_passes_lines_inside_a_code_element(self):
self.assertEqual([], rules("/// \n/// var thing = Make()\n/// Use(thing)\n/// \n"))
def test_treats_two_markers_as_separate_blocks(self):
self.assertEqual([], rules("/// Summary text.\n// Implementation note.\n"))
def test_treats_two_indents_as_separate_blocks(self):
self.assertEqual([], rules("// Outer note.\n // Inner note.\n"))
def test_counts_a_tab_as_four_columns(self):
first = "\t" + padded("// Filled", 112)
source = first + "\n\t// abcdef continues.\n"
self.assertEqual([], rules(source))
def test_skips_an_untouched_block(self):
source = "// Short.\n// Then more.\n\nint x;\n"
self.assertEqual([], rules(source, touched={3}))
def test_checks_the_whole_block_when_one_line_is_touched(self):
source = "// Short.\n// Then more.\n// And more.\n"
self.assertEqual([1, 2], [issue.line for issue in lint.lint(source, touched={2})])
class WidthTests(unittest.TestCase):
def test_flags_a_comment_past_the_limit(self):
self.assertEqual(["comment-width"], rules(padded("// Long", 121) + "\n"))
def test_passes_a_comment_at_the_limit(self):
self.assertEqual([], rules(padded("// Long", 120) + "\n"))
def test_passes_a_url(self):
self.assertEqual([], rules("// See https://example.com/" + "a" * 120 + " for details.\n"))
def test_passes_one_unbreakable_token(self):
self.assertEqual([], rules("// " + "a" * 130 + "\n"))
class LineWidthTests(unittest.TestCase):
def test_flags_code_past_the_limit(self):
self.assertEqual(["line-width"], rules(" " + padded("int x = Call(", 113) + "\n"))
def test_passes_code_at_the_limit(self):
self.assertEqual([], rules(" " + padded("int x = Call(", 112) + "\n"))
def test_flags_code_with_a_trailing_comment_past_the_limit(self):
self.assertEqual(["line-width"], rules("int x = 1; " + padded("// trailing", 120) + "\n"))
def test_reports_the_column_count(self):
issues = lint.lint("int x = Call(" + " a" * 60 + ");\n")
self.assertEqual(1, len(issues))
self.assertIn("is 135 columns", issues[0].message)
def test_counts_a_tab_as_four_columns(self):
self.assertEqual(["line-width"], rules("\t\t" + padded("int x = Call(", 113) + "\n"))
self.assertEqual([], rules("\t\t" + padded("int x = Call(", 112) + "\n"))
def test_leaves_a_comment_line_to_the_comment_rule(self):
self.assertEqual(["comment-width"], rules(padded("// Long", 121) + "\n"))
def test_passes_a_line_with_no_spaces_to_split_at(self):
self.assertEqual([], rules(" " + "a" * 130 + ";\n"))
def test_passes_the_inside_of_a_multiline_string(self):
text = 'var text = """\n' + padded("some words", 130) + '\n""";\n'
self.assertEqual([], rules(text))
def test_skips_an_untouched_line(self):
self.assertEqual([], rules("int x = Call(" + " a" * 60 + ");\n", touched=set()))
class SummaryLineTests(unittest.TestCase):
def test_flags_text_beside_summary_tags(self):
self.assertEqual(["summary-own-line"], rules("/// Returns the thing.\nint X();\n"))
def test_flags_a_closing_tag_after_text(self):
source = "/// \n/// Returns the thing.\nint X();\n"
self.assertEqual(["summary-own-line"], rules(source))
def test_passes_tags_on_their_own_lines(self):
self.assertEqual([], rules("/// \n/// Returns the thing.\n/// \nint X();\n"))
class DocStarterTests(unittest.TestCase):
def test_flags_a_summary_naming_what_it_documents(self):
source = "/// \n/// This method returns the thing.\n/// \nint X();\n"
issues = lint.lint(source)
self.assertEqual(["doc-starter"], [issue.rule for issue in issues])
self.assertEqual(2, issues[0].line)
def test_passes_a_summary_starting_with_its_verb(self):
self.assertEqual([], rules("/// \n/// Returns the thing.\n/// \nint X();\n"))
def test_passes_a_comment_that_isnt_a_doc_comment(self):
self.assertEqual([], rules("// This method is slow on purpose.\nint X();\n"))
class FieldStarterTests(unittest.TestCase):
def check(self, summary, declaration):
return rules("/// \n/// %s\n/// \n%s\n" % (summary, declaration))
def test_flags_holds_on_a_field(self):
self.assertEqual(["field-doc-starter"], self.check("Holds the count.", "public int Count;"))
def test_flags_gets_on_a_constant(self):
self.assertEqual(["field-doc-starter"], self.check("Gets the limit.", "public const int Limit = 4;"))
def test_flags_a_field_initialized_by_a_call(self):
self.assertEqual(["field-doc-starter"],
self.check("Holds the names.", "private readonly List names = new List();"))
def test_skips_attributes_to_find_the_field(self):
self.assertEqual(["field-doc-starter"], self.check("Holds the count.", "[NoSave]\npublic int Count;"))
def test_passes_a_bare_phrase_on_a_field(self):
self.assertEqual([], self.check("The count of items.", "public int Count;"))
def test_passes_gets_on_a_property(self):
self.assertEqual([], self.check("Gets the count.", "public int Count { get; }"))
def test_passes_returns_on_a_method(self):
self.assertEqual([], self.check("Returns the count.", "public int Count();"))
def test_passes_an_expression_bodied_member(self):
self.assertEqual([], self.check("Gets the count.", "public int Count => items.Count;"))
def test_passes_a_delegate(self):
self.assertEqual([], self.check("Holds nothing.", "public delegate void Handler(int value);"))
class JoinerTests(unittest.TestCase):
def test_flags_a_semicolon(self):
self.assertEqual(["comment-joiner"], rules("// It is fast; it is small.\n"))
def test_flags_an_em_dash(self):
self.assertEqual(["comment-joiner"], rules("// It is fast \u2014 and small.\n"))
def test_flags_a_double_hyphen(self):
self.assertEqual(["comment-joiner"], rules("// It is fast -- and small.\n"))
def test_flags_a_spaced_hyphen_between_words(self):
self.assertEqual(["comment-joiner"], rules("// It is fast - and small.\n"))
def test_passes_a_minus_between_single_letters(self):
self.assertEqual([], rules("// The offset is x - y from the start.\n"))
def test_passes_a_minus_in_a_line_with_math(self):
self.assertEqual([], rules("// So total = start - offset holds.\n"))
def test_passes_a_semicolon_in_code_spans(self):
source = "// Writes `a; b` and \"c; d\" and e; f and ';' alike, escaping < too.\n"
self.assertEqual([], rules(source))
def test_passes_commented_out_code(self):
self.assertEqual([], rules("// for (int i = 0; i < n; ++i)\n"))
def test_passes_a_list_item_marker(self):
self.assertEqual([], rules("// Things.\n// - a thing\n"))
class MaskTests(unittest.TestCase):
def code_left(self, source):
"""Returns what masking leaves of source, without whitespace, after checking it keeps every position."""
masked = lint.mask_code(source)
self.assertEqual(len(source), len(masked))
self.assertEqual(source.count("\n"), masked.count("\n"))
return "".join(masked.split())
def test_blanks_comments(self):
self.assertEqual("x;", self.code_left("x; // if (a) b\n"))
self.assertEqual("x;y;", self.code_left("x; /* {\n}*/y;"))
def test_blanks_strings_and_chars(self):
self.assertEqual("a(,);", self.code_left('a("if (x)", \'{\');'))
def test_blanks_escaped_quotes(self):
self.assertEqual("a(,);", self.code_left('a("\\"{\\"", \'\\\'\');'))
def test_blanks_verbatim_and_interpolated_strings(self):
self.assertEqual("a(,,);", self.code_left('a(@"x"" { y", $"{b} {{", $@"{c}");'))
def test_blanks_a_raw_string_across_lines(self):
self.assertEqual("a();", self.code_left('a("""\n{ "" \n""");'))
def test_blanks_preprocessor_lines(self):
self.assertEqual("x;", self.code_left("#if DEBUG\nx;\n #endregion"))
class BracesTests(unittest.TestCase):
def test_flags_an_unbraced_if(self):
issues = lint.lint("if (a)\n b();\n")
self.assertEqual(["braces"], [issue.rule for issue in issues])
self.assertEqual(1, issues[0].line)
def test_flags_an_if_on_one_line(self):
self.assertEqual(["braces"], rules("if (a) return;\n"))
def test_flags_each_kind_of_statement(self):
for statement in ("for (;;)", "foreach (var x in xs)", "while (a)", "using (var x = Open())", "lock (gate)",
"fixed (int* p = array)", "else", "do"):
self.assertEqual(["braces"], rules("%s\n b();\n" % statement), statement)
def test_passes_braced_bodies(self):
self.assertEqual([], rules("if (a)\n{\n b();\n}\n"))
self.assertEqual([], rules("do\n{\n b();\n}\nwhile (a);\n"))
def test_passes_else_if(self):
self.assertEqual([], rules("if (a)\n{\n}\nelse if (b)\n{\n}\n"))
def test_finds_the_end_of_a_condition_spanning_lines(self):
self.assertEqual([], rules("if (a &&\n (b || c))\n{\n}\n"))
self.assertEqual(["braces"], rules("if (a &&\n (b || c))\n d();\n"))
def test_passes_a_do_while_tail(self):
self.assertEqual([], rules("do\n{\n}\nwhile (a);\n"))
def test_passes_stacked_usings(self):
self.assertEqual([], rules("using (var a = A())\nusing (var b = B())\n{\n}\n"))
def test_flags_the_last_of_stacked_usings_without_braces(self):
self.assertEqual(["braces"], rules("using (var a = A())\nusing (var b = B())\n c();\n"))
def test_passes_using_directives_and_declarations(self):
self.assertEqual([], rules("using System;\nusing static System.Math;\nusing var x = Open();\n"))
def test_passes_a_fixed_size_buffer(self):
self.assertEqual([], rules("unsafe fixed byte buffer[16];\n"))
def test_passes_keywords_in_strings_comments_and_names(self):
self.assertEqual([], rules('Log("if (a) b");\n// if (a) b\nvar ifx = x.@if;\ndouble d;\n'))
def test_passes_preprocessor_conditions(self):
self.assertEqual([], rules("#if DEBUG\nx();\n#else\ny();\n#endif\n"))
def test_reports_only_touched_statements(self):
source = "if (a)\n b();\nif (c)\n d();\n"
self.assertEqual([3], [issue.line for issue in lint.lint(source, touched={3})])
class BlankAfterBlockTests(unittest.TestCase):
def test_flags_code_right_after_a_block(self):
issues = lint.lint("if (a)\n{\n}\nb();\n")
self.assertEqual(["blank-after-block"], [issue.rule for issue in issues])
self.assertEqual(3, issues[0].line)
def test_passes_a_blank_line_after_a_block(self):
self.assertEqual([], rules("if (a)\n{\n}\n\nb();\n"))
def test_passes_what_continues_a_block(self):
for following in ("}", "else", "catch (Exception)", "finally", "while (a);", "case 1:", "default:", "get",
"set", ");", ", other", "]", "#endif", "#endregion"):
source = "{\n {\n }\n %s\n}\n" % following
self.assertNotIn("blank-after-block", rules(source), following)
def test_flags_a_comment_right_after_a_block(self):
self.assertEqual(["blank-after-block"], rules("{\n}\n// Next.\n"))
def test_passes_a_brace_ending_an_initializer(self):
self.assertEqual([], rules("var x = new X\n{\n};\nb();\n"))
def test_reports_when_only_the_following_line_is_touched(self):
self.assertEqual(["blank-after-block"], rules("{\n}\nb();\n", touched={2}))
class InternalTests(unittest.TestCase):
def test_flags_internal(self):
self.assertEqual(["internal"], rules("internal class Thing\n{\n}\n"))
self.assertEqual(["internal"], rules("protected internal void Run();\n"))
def test_passes_internal_in_names_strings_and_comments(self):
self.assertEqual([], rules('[assembly: InternalsVisibleTo("x")]\n// internal\nvar internalCount = 0;\n'))
def test_passes_the_compiler_polyfill(self):
self.assertEqual([], rules("internal static class IsExternalInit\n{\n}\n"))
class GenericNameTests(unittest.TestCase):
def test_flags_a_type_parameter_on_a_type(self):
self.assertEqual(["generic-name"], rules("public class Box\n{\n}\n"))
def test_flags_each_bad_parameter(self):
self.assertEqual(["generic-name", "generic-name"], rules("public struct Pair\n{\n}\n\n"
"public interface IMap\n{\n}\n"))
def test_flags_a_type_parameter_on_a_method(self):
self.assertEqual(["generic-name"], rules("public T Make(int count)\n"))
self.assertEqual(["generic-name"], rules("void Local(string key);\n"))
def test_flags_variance_and_attributes_by_name(self):
self.assertEqual(["generic-name"], rules("public interface ISource\n{\n}\n"))
def test_flags_the_bare_word_type(self):
self.assertEqual(["generic-name"], rules("public class Box\n{\n}\n"))
def test_passes_names_ending_in_type(self):
self.assertEqual([], rules("public class Arc\n{\n}\n"))
self.assertEqual([], rules("public ItemType Make(int count)\n"))
def test_passes_generic_uses(self):
source = ("var items = new List();\nreturn Make(1);\nvar x = Make(1);\n"
"if (Is(x))\n{\n}\n\nthing.Make(1);\nList Build(int count)\n")
self.assertEqual([], rules(source))
class TouchedTests(unittest.TestCase):
def test_write_of_a_new_file_touches_everything(self):
self.assertIsNone(lint.touched_by_write(None, "a\nb\n"))
def test_write_touches_only_changed_lines(self):
self.assertEqual({1, 3}, lint.touched_by_write("a\nb\nc\nd\n", "a\nB\nc\nD\n"))
def test_write_ignores_line_ending_changes(self):
self.assertEqual(set(), lint.touched_by_write("a\r\nb\r\n", "a\nb\n"))
def test_edit_touches_where_the_new_string_landed(self):
self.assertEqual({1, 2}, lint.touched_by_edit("a\nnew one\nnew two\nd\n", ["new one\nnew two"]))
def test_edit_matches_an_lf_string_in_a_crlf_file(self):
self.assertEqual({1, 2}, lint.touched_by_edit("a\r\nnew one\r\nnew two\r\n", ["new one\nnew two"]))
def test_edit_touches_every_place_the_new_string_appears(self):
self.assertEqual({0, 2}, lint.touched_by_edit("same\nother\nsame\n", ["same"]))
def test_deletion_touches_nothing(self):
self.assertEqual(set(), lint.touched_by_edit("a\nb\n", [""]))
class PathTests(unittest.TestCase):
def test_lints_source(self):
self.assertTrue(lint.is_linted_path("C:/ElectricRoot/Modules/Glass/Glass/Control.cs"))
def test_skips_other_languages(self):
self.assertFalse(lint.is_linted_path("C:/ElectricRoot/Scripts/build.py"))
def test_skips_generated_and_vendored_code(self):
self.assertFalse(lint.is_linted_path("C:/ElectricRoot/Junk/Glass/Release/Thing.cs"))
self.assertFalse(lint.is_linted_path("C:\\ElectricRoot\\3rdParty\\Lib\\Thing.cs"))
self.assertFalse(lint.is_linted_path("C:/ElectricRoot/Modules/A/Form1.Designer.cs"))
self.assertFalse(lint.is_linted_path("C:/ElectricRoot/Modules/A/Properties/AssemblyInfo.cs"))
self.assertFalse(lint.is_linted_path("C:/ElectricRoot/Modules/Ariadne/A.UnitTests/Scripts.CodeBehind.cs"))
class HookTests(unittest.TestCase):
def setUp(self):
handle, self.path = tempfile.mkstemp(suffix=".cs")
os.close(handle)
def tearDown(self):
os.remove(self.path)
def write(self, text):
with open(self.path, "w", encoding="utf-8", newline="") as source:
source.write(text)
def test_edit_reports_its_own_lines(self):
self.write("// Short.\n// Then more.\n\n// Also short.\n// And more.\n")
payload = {"tool_name": "Edit", "tool_input": {"file_path": self.path, "new_string": "// Also short."}}
report = lint.run_hook(payload)
self.assertIn("line 4 [comment-fill]", report)
self.assertNotIn("line 1 ", report)
def test_write_of_a_new_file_reports_every_line(self):
text = "// Short.\n// Then more.\n"
self.write(text)
payload = {"tool_name": "Write", "tool_input": {"file_path": self.path, "content": text},
"tool_response": {"originalFile": None}}
self.assertIn("line 1 [comment-fill]", lint.run_hook(payload))
def test_multi_edit_reports_each_edit(self):
self.write("// Short.\n// Then more.\n\n// Also short.\n// And more.\n")
payload = {"tool_name": "MultiEdit", "tool_input": {"file_path": self.path, "edits": [
{"new_string": "// Short."}, {"new_string": "// And more."}]}}
report = lint.run_hook(payload)
self.assertIn("line 1 [comment-fill]", report)
self.assertIn("line 4 [comment-fill]", report)
def test_clean_edit_reports_nothing(self):
self.write("// One line.\n")
payload = {"tool_name": "Edit", "tool_input": {"file_path": self.path, "new_string": "// One line."}}
self.assertIsNone(lint.run_hook(payload))
def test_other_tools_report_nothing(self):
self.assertIsNone(lint.run_hook({"tool_name": "Bash", "tool_input": {"command": "ls"}}))
def test_caps_a_long_report(self):
self.write("".join("// Short %d.\n// More.\n//\n" % i for i in range(lint.MAX_REPORTED + 5)))
payload = {"tool_name": "Write", "tool_input": {"file_path": self.path, "content": ""},
"tool_response": {"originalFile": None}}
report = lint.run_hook(payload)
self.assertIn("...and 5 more.", report)
if __name__ == "__main__":
unittest.main()