|
| 1 | +from .Slice import Slice |
| 2 | +from .Literals import Language, LiteralFormat, LiteralValue |
| 3 | +from .EscapeLeadingWhitespace import EscapeLeadingWhitespace |
| 4 | +from typing import Any |
| 5 | + |
| 6 | + |
| 7 | +class SourceFile: |
| 8 | + TRIPLE_QUOTE = '"""' |
| 9 | + |
| 10 | + def __init__(self, filename: str, content: str) -> None: |
| 11 | + self.__unix_newlines: bool = "\r" not in content |
| 12 | + self.__content_slice: Slice = Slice(content.replace("\r\n", "\n")) |
| 13 | + self.__language: Language = Language.from_filename(filename) |
| 14 | + self.__escape_leading_whitespace = EscapeLeadingWhitespace.appropriate_for( |
| 15 | + self.__content_slice.__str__() |
| 16 | + ) |
| 17 | + |
| 18 | + @property |
| 19 | + def as_string(self) -> str: |
| 20 | + return ( |
| 21 | + self.__content_slice.__str__() |
| 22 | + if self.__unix_newlines |
| 23 | + else self.__content_slice.__str__().replace("\n", "\r\n") |
| 24 | + ) |
| 25 | + |
| 26 | + class ToBeLiteral: |
| 27 | + def __init__( |
| 28 | + self, |
| 29 | + dot_fun_open_paren: str, |
| 30 | + function_call_plus_arg: Slice, |
| 31 | + arg: Slice, |
| 32 | + language: Language, |
| 33 | + escape_leading_whitespace: EscapeLeadingWhitespace, |
| 34 | + ) -> None: |
| 35 | + self.__dot_fun_open_paren = dot_fun_open_paren |
| 36 | + self.__function_call_plus_arg = function_call_plus_arg |
| 37 | + self.__arg = arg |
| 38 | + self.__language = language |
| 39 | + self.__escape_leading_whitespace = escape_leading_whitespace |
| 40 | + |
| 41 | + def _get_function_call_plus_arg(self): |
| 42 | + return self.__function_call_plus_arg |
| 43 | + |
| 44 | + def _get_arg(self): |
| 45 | + return self.__arg |
| 46 | + |
| 47 | + def set_literal_and_get_newline_delta(self, literal_value: LiteralValue) -> int: |
| 48 | + encoded = literal_value.format.encode( |
| 49 | + literal_value.actual, self.__language, self.__escape_leading_whitespace |
| 50 | + ) |
| 51 | + round_tripped = literal_value.format.parse(encoded, self.__language) |
| 52 | + if round_tripped != literal_value.actual: |
| 53 | + raise ValueError( |
| 54 | + f"There is an error in {literal_value.format.__class__.__name__}, " |
| 55 | + "the following value isn't round tripping.\n" |
| 56 | + f"Please report this error and the data below at " |
| 57 | + "https://github.com/diffplug/selfie/issues/new\n" |
| 58 | + f"```\n" |
| 59 | + f"ORIGINAL\n{literal_value.actual}\n" |
| 60 | + f"ROUNDTRIPPED\n{round_tripped}\n" |
| 61 | + f"ENCODED ORIGINAL\n{encoded}\n" |
| 62 | + f"```\n" |
| 63 | + ) |
| 64 | + existing_newlines = self.__function_call_plus_arg.count("\n") |
| 65 | + new_newlines = encoded.count("\n") |
| 66 | + self.__content_slice = self.__function_call_plus_arg.replaceSelfWith( |
| 67 | + f"{self.__dot_fun_open_paren}{encoded})" |
| 68 | + ) |
| 69 | + return new_newlines - existing_newlines |
| 70 | + |
| 71 | + def parse_literal(self, literal_format: LiteralFormat) -> Any: |
| 72 | + return literal_format.parse(self.__arg.__str__(), self.__language) |
| 73 | + |
| 74 | + def find_on_line(self, to_find: str, line_one_indexed: int) -> Slice: |
| 75 | + line_content = self.__content_slice.unixLine(line_one_indexed) |
| 76 | + idx = line_content.indexOf(to_find) |
| 77 | + if idx == -1: |
| 78 | + raise AssertionError( |
| 79 | + f"Expected to find `{to_find}` on line {line_one_indexed}, " |
| 80 | + f"but there was only `{line_content}`" |
| 81 | + ) |
| 82 | + start_index = idx |
| 83 | + end_index = idx + len(to_find) |
| 84 | + return line_content.subSequence(start_index, end_index) |
| 85 | + |
| 86 | + def replace_on_line(self, line_one_indexed: int, find: str, replace: str) -> None: |
| 87 | + assert "\n" not in find |
| 88 | + assert "\n" not in replace |
| 89 | + line_content = self.__content_slice.unixLine(line_one_indexed).__str__() |
| 90 | + new_content = line_content.replace(find, replace) |
| 91 | + self.__content_slice = Slice(self.__content_slice.replaceSelfWith(new_content)) |
| 92 | + |
| 93 | + def parse_to_be_like(self, line_one_indexed: int) -> ToBeLiteral: |
| 94 | + line_content = self.__content_slice.unixLine(line_one_indexed) |
| 95 | + dot_fun_open_paren = None |
| 96 | + |
| 97 | + for to_be_like in TO_BE_LIKES: |
| 98 | + idx = line_content.indexOf(to_be_like) |
| 99 | + if idx != -1: |
| 100 | + dot_fun_open_paren = to_be_like |
| 101 | + break |
| 102 | + if dot_fun_open_paren is None: |
| 103 | + raise AssertionError( |
| 104 | + f"Expected to find inline assertion on line {line_one_indexed}, but there was only `{line_content}`" |
| 105 | + ) |
| 106 | + |
| 107 | + dot_function_call_in_place = line_content.indexOf(dot_fun_open_paren) |
| 108 | + dot_function_call = dot_function_call_in_place + line_content.startIndex |
| 109 | + arg_start = dot_function_call + len(dot_fun_open_paren) |
| 110 | + |
| 111 | + if self.__content_slice.__len__() == arg_start: |
| 112 | + raise AssertionError( |
| 113 | + f"Appears to be an unclosed function call `{dot_fun_open_paren}` " |
| 114 | + f"on line {line_one_indexed}" |
| 115 | + ) |
| 116 | + while self.__content_slice[arg_start].isspace(): |
| 117 | + arg_start += 1 |
| 118 | + if self.__content_slice.__len__() == arg_start: |
| 119 | + raise AssertionError( |
| 120 | + f"Appears to be an unclosed function call `{dot_fun_open_paren}` " |
| 121 | + f"on line {line_one_indexed}" |
| 122 | + ) |
| 123 | + |
| 124 | + end_arg = -1 |
| 125 | + end_paren = 0 |
| 126 | + if self.__content_slice[arg_start] == '"': |
| 127 | + if self.__content_slice[arg_start].startswith(self.TRIPLE_QUOTE): |
| 128 | + end_arg = self.__content_slice.indexOf( |
| 129 | + self.TRIPLE_QUOTE, arg_start + len(self.TRIPLE_QUOTE) |
| 130 | + ) |
| 131 | + if end_arg == -1: |
| 132 | + raise AssertionError( |
| 133 | + f"Appears to be an unclosed multiline string literal `{self.TRIPLE_QUOTE}` " |
| 134 | + f"on line {line_one_indexed}" |
| 135 | + ) |
| 136 | + else: |
| 137 | + end_arg += len(self.TRIPLE_QUOTE) |
| 138 | + end_paren = end_arg |
| 139 | + else: |
| 140 | + end_arg = arg_start + 1 |
| 141 | + while ( |
| 142 | + self.__content_slice[end_arg] != '"' |
| 143 | + or self.__content_slice[end_arg - 1] == "\\" |
| 144 | + ): |
| 145 | + end_arg += 1 |
| 146 | + if end_arg == self.__content_slice.__len__(): |
| 147 | + raise AssertionError( |
| 148 | + f'Appears to be an unclosed string literal `"` ' |
| 149 | + f"on line {line_one_indexed}" |
| 150 | + ) |
| 151 | + end_arg += 1 |
| 152 | + end_paren = end_arg |
| 153 | + else: |
| 154 | + end_arg = arg_start |
| 155 | + while not self.__content_slice[end_arg].isspace(): |
| 156 | + if self.__content_slice[end_arg] == ")": |
| 157 | + break |
| 158 | + end_arg += 1 |
| 159 | + if end_arg == self.__content_slice.__len__(): |
| 160 | + raise AssertionError( |
| 161 | + f"Appears to be an unclosed numeric literal " |
| 162 | + f"on line {line_one_indexed}" |
| 163 | + ) |
| 164 | + end_paren = end_arg |
| 165 | + while self.__content_slice[end_paren] != ")": |
| 166 | + if not self.__content_slice[end_paren].isspace(): |
| 167 | + raise AssertionError( |
| 168 | + f"Non-primitive literal in `{dot_fun_open_paren}` starting at " |
| 169 | + f"line {line_one_indexed}: error for character " |
| 170 | + f"`{self.__content_slice[end_paren]}` on line " |
| 171 | + f"{self.__content_slice.baseLineAtOffset(end_paren)}" |
| 172 | + ) |
| 173 | + end_paren += 1 |
| 174 | + if end_paren == self.__content_slice.__len__(): |
| 175 | + raise AssertionError( |
| 176 | + f"Appears to be an unclosed function call `{dot_fun_open_paren}` " |
| 177 | + f"starting at line {line_one_indexed}" |
| 178 | + ) |
| 179 | + return self.ToBeLiteral( |
| 180 | + dot_fun_open_paren.replace("_TODO", ""), |
| 181 | + self.__content_slice.subSequence(dot_function_call, end_paren + 1), |
| 182 | + self.__content_slice.subSequence(arg_start, end_arg), |
| 183 | + self.__language, |
| 184 | + self.__escape_leading_whitespace, |
| 185 | + ) |
| 186 | + |
| 187 | + |
| 188 | +TO_BE_LIKES = [".toBe(", ".toBe_TODO(", ".toBeBase64(", ".toBeBase64_TODO("] |
0 commit comments