Appearance
| 1 | namespace Syntax.Process is | |
| 2 | use Source.LOCATION; | |
| 3 | use Trees; | |
| 4 | ||
| 5 | use Logging.Logger; | |
| 6 | ||
| 7 | // Walks the AST after declare-symbols and emits warnings for | |
| 8 | // ghūl-source declarations whose names don't match the case | |
| 9 | // convention for their kind: | |
| 10 | // | |
| 11 | // snake_case — locals (via `let`, `for`, `catch`, | |
| 12 | // function arguments), fields, properties, | |
| 13 | // methods, free functions. | |
| 14 | // Slug: non-snake-case-name. | |
| 15 | // | |
| 16 | // PascalCase — abstract classes, traits, unions, enums. | |
| 17 | // Slug: non-pascal-case-name. | |
| 18 | // | |
| 19 | // UPPER_SNAKE_CASE — concrete (instantiable) classes, structs, | |
| 20 | // variants, enum members. | |
| 21 | // Slug: non-upper-snake-case-name. | |
| 22 | // | |
| 23 | // Each is independently suppressable via the standard | |
| 24 | // `@suppress("<slug>")` pragma — pin per declaration, per file, | |
| 25 | // or project-wide as the project's conventions require. Unit-test | |
| 26 | // projects with the `Calling__foo__should_X` long-name idiom are | |
| 27 | // the typical project-wide suppression for non-snake-case-name. | |
| 28 | // | |
| 29 | // What this pass DOESN'T flag: | |
| 30 | // - User-defined operator names like `+`, `==`, `##`, `\/` — | |
| 31 | // the tokenizer emits them as IDENTIFIER too but they have | |
| 32 | // no case-style form. Filtered by the leading-character | |
| 33 | // check below. | |
| 34 | // - Compiler-synthesised `$`-prefixed names. | |
| 35 | // - Imported .NET members — the symbol factory auto-converts | |
| 36 | // them; this pass only sees ghūl-source AST. | |
| 37 | class CHECK_NAME_CONVENTIONS: Visitor is | |
| 38 | _logger: Logger; | |
| 39 | _symbol_table: Semantic.SYMBOL_TABLE; | |
| 40 | ||
| 41 | init(logger: Logger, symbol_table: Semantic.SYMBOL_TABLE) is | |
| 42 | super.init(); | |
| 43 | ||
| 44 | _logger = logger; | |
| 45 | _symbol_table = symbol_table; | |
| 46 | si | |
| 47 | ||
| 48 | apply(source_file: Compiler.SOURCE_FILE) is | |
| 49 | source_file.definition.walk(self); | |
| 50 | si | |
| 51 | ||
| 52 | visit(variable: Trees.Variables.VARIABLE) is | |
| 53 | // A static field is a named constant and conventionally | |
| 54 | // UPPER_SNAKE_CASE; a snake_case static field is fine too. | |
| 55 | // Instance fields and locals stay snake_case. | |
| 56 | for identifier in variable.left.names! do | |
| 57 | if variable.is_static then | |
| 58 | _check_snake_or_upper_snake(identifier, "name"); | |
| 59 | else | |
| 60 | _check_snake(identifier, "name"); | |
| 61 | fi | |
| 62 | od | |
| 63 | si | |
| 64 | ||
| 65 | visit(function: Trees.Definitions.FUNCTION) is | |
| 66 | if !function.name? then | |
| 67 | return; | |
| 68 | fi | |
| 69 | ||
| 70 | // Skip compiler-synthesised accessor functions. Property | |
| 71 | // accessors carry a `$` prefix that the leading-character | |
| 72 | // check filters; indexer accessors must use the | |
| 73 | // CLR-required `get_<Name>` / `set_<Name>` shape and | |
| 74 | // aren't named by the user, so we gate on the AST link. | |
| 75 | if function.for_property? \/ function.for_indexer? then | |
| 76 | return; | |
| 77 | fi | |
| 78 | ||
| 79 | _check_snake(function.name, "name"); | |
| 80 | si | |
| 81 | ||
| 82 | visit(property: Trees.Definitions.PROPERTY) is | |
| 83 | if !property.name? then | |
| 84 | return; | |
| 85 | fi | |
| 86 | ||
| 87 | // A static property may name a constant and so may be | |
| 88 | // UPPER_SNAKE_CASE; snake_case is fine too. | |
| 89 | if property.modifiers.is_static then | |
| 90 | _check_snake_or_upper_snake(property.name, "name"); | |
| 91 | else | |
| 92 | _check_snake(property.name, "name"); | |
| 93 | fi | |
| 94 | si | |
| 95 | ||
| 96 | visit(`class: Trees.Definitions.CLASS) is | |
| 97 | ||
| 98 | // Abstract classes follow PascalCase. Abstractness is read | |
| 99 | // from the declared symbol so both explicitly-`abstract` | |
| 100 | // classes and implicitly-abstract ones (a class with a | |
| 101 | // user-written body-less instance method, marked during | |
| 102 | // declare-symbols) are recognised uniformly — this pass runs | |
| 103 | // after declare-symbols for that reason. A class with no | |
| 104 | // primary-constructor parameters whose every instance-shaped | |
| 105 | // member is `static` is a static-utility container — never | |
| 106 | // constructed — and conventionally either PascalCase or | |
| 107 | // UPPER_SNAKE_CASE, so either passes. | |
| 108 | if _is_abstract_class(`class) then | |
| 109 | _check_pascal(`class.name, "abstract class"); | |
| 110 | elif _is_static_only_class(`class) then | |
| 111 | _check_pascal_or_upper_snake(`class.name, "class"); | |
| 112 | else | |
| 113 | _check_upper_snake(`class.name, "class"); | |
| 114 | fi | |
| 115 | si | |
| 116 | ||
| 117 | visit(`struct: Trees.Definitions.STRUCT) is | |
| 118 | ||
| 119 | _check_upper_snake(`struct.name, "struct"); | |
| 120 | si | |
| 121 | ||
| 122 | visit(`trait: Trees.Definitions.TRAIT) is | |
| 123 | ||
| 124 | _check_pascal(`trait.name, "trait"); | |
| 125 | si | |
| 126 | ||
| 127 | visit(`union: Trees.Definitions.UNION) is | |
| 128 | ||
| 129 | _check_pascal(`union.name, "union"); | |
| 130 | si | |
| 131 | ||
| 132 | visit(variant: Trees.Definitions.VARIANT) is | |
| 133 | _check_upper_snake(variant.name, "variant"); | |
| 134 | si | |
| 135 | ||
| 136 | visit(`enum: Trees.Definitions.ENUM) is | |
| 137 | ||
| 138 | _check_pascal(`enum.name, "enum"); | |
| 139 | si | |
| 140 | ||
| 141 | visit(enum_member: Trees.Definitions.ENUM_MEMBER) is | |
| 142 | ||
| 143 | _check_upper_snake(enum_member.name, "enum member"); | |
| 144 | si | |
| 145 | ||
| 146 | _check_snake(identifier: Trees.Identifiers.Identifier, kind: string) is | |
| 147 | let bare = _prepare(identifier); | |
| 148 | ||
| 149 | if !bare? \/ _is_snake_case(bare) then | |
| 150 | return; | |
| 151 | fi | |
| 152 | ||
| 153 | _warn(identifier, kind, "non-snake-case-name", "snake_case"); | |
| 154 | si | |
| 155 | ||
| 156 | _check_pascal(identifier: Trees.Identifiers.Identifier, kind: string) is | |
| 157 | let bare = _prepare(identifier); | |
| 158 | ||
| 159 | if !bare? \/ _is_pascal_case(bare) then | |
| 160 | return; | |
| 161 | fi | |
| 162 | ||
| 163 | _warn(identifier, kind, "non-pascal-case-name", "PascalCase"); | |
| 164 | si | |
| 165 | ||
| 166 | _check_upper_snake(identifier: Trees.Identifiers.Identifier, kind: string) is | |
| 167 | let bare = _prepare(identifier); | |
| 168 | ||
| 169 | if !bare? \/ _is_upper_snake_case(bare) then | |
| 170 | return; | |
| 171 | fi | |
| 172 | ||
| 173 | _warn(identifier, kind, "non-upper-snake-case-name", "UPPER_SNAKE_CASE"); | |
| 174 | si | |
| 175 | ||
| 176 | _check_pascal_or_upper_snake(identifier: Trees.Identifiers.Identifier, kind: string) is | |
| 177 | let bare = _prepare(identifier); | |
| 178 | ||
| 179 | if !bare? \/ _is_pascal_case(bare) \/ _is_upper_snake_case(bare) then | |
| 180 | return; | |
| 181 | fi | |
| 182 | ||
| 183 | _warn(identifier, kind, "non-upper-snake-case-name", "UPPER_SNAKE_CASE"); | |
| 184 | si | |
| 185 | ||
| 186 | _check_snake_or_upper_snake(identifier: Trees.Identifiers.Identifier, kind: string) is | |
| 187 | let bare = _prepare(identifier); | |
| 188 | ||
| 189 | if !bare? \/ _is_snake_case(bare) \/ _is_upper_snake_case(bare) then | |
| 190 | return; | |
| 191 | fi | |
| 192 | ||
| 193 | _warn(identifier, kind, "non-snake-case-name", "snake_case"); | |
| 194 | si | |
| 195 | ||
| 196 | // A class is abstract — and so follows PascalCase — when its | |
| 197 | // declared symbol reports `is_abstract`, which folds together an | |
| 198 | // explicit `abstract` modifier and implicit abstractness from a | |
| 199 | // user-written body-less instance method. The symbol is reached | |
| 200 | // through the node-to-scope map populated by declare-symbols; | |
| 201 | // the syntactic modifier is a fallback for the rare case where | |
| 202 | // the class has no associated symbol. | |
| 203 | _is_abstract_class(`class: Trees.Definitions.CLASS) -> bool is | |
| 204 | let classy = cast Semantic.Symbols.Classy?(_symbol_table.scope_for(`class)); | |
| 205 | ||
| 206 | if classy? then | |
| 207 | return classy.is_abstract; | |
| 208 | fi | |
| 209 | ||
| 210 | return `class.modifiers.is_abstract; | |
| 211 | si | |
| 212 | ||
| 213 | // Returns the bare identifier (backticks stripped) when it | |
| 214 | // starts with a letter or underscore — i.e. is in scope for | |
| 215 | // any case-style rule. Returns null when the identifier is | |
| 216 | // an operator, a `$`-prefixed compiler-synthesised name, or | |
| 217 | // would be empty after stripping. Backticks themselves are | |
| 218 | // escape/disambiguation syntax (`\`field\``, `list\`[T]`) and | |
| 219 | // not part of the name semantically. | |
| 220 | _prepare(identifier: Trees.Identifiers.Identifier) -> string? static is | |
| 221 | if identifier.name.length == 0 then | |
| 222 | return null; | |
| 223 | fi | |
| 224 | ||
| 225 | let bare = _strip_backticks(identifier.name); | |
| 226 | ||
| 227 | if bare.length == 0 \/ !_starts_with_identifier_char(bare) then | |
| 228 | return null; | |
| 229 | fi | |
| 230 | ||
| 231 | return bare; | |
| 232 | si | |
| 233 | ||
| 234 | // A class is "static-only" — and therefore not meaningfully | |
| 235 | // instantiable — when it has no primary-constructor params | |
| 236 | // and every instance-shaped member (function, property, field) | |
| 237 | // is `static`. Nested type definitions don't count either way; | |
| 238 | // a class containing only nested types is also non-instantiable. | |
| 239 | // An empty class is *not* considered static-only: a bare | |
| 240 | // `class FOO is si` is presumed to be a soon-to-be-populated | |
| 241 | // instantiable shell. | |
| 242 | _is_static_only_class(`class: Trees.Definitions.CLASS) -> bool static is | |
| 243 | if let `class.primary_params? /\ primary_params.count > 0 then | |
| 244 | return false; | |
| 245 | fi | |
| 246 | ||
| 247 | ||
| 248 | let saw_instance_shaped mut = false; | |
| 249 | ||
| 250 | for member in `class.body do | |
| 251 | let bare = member.without_pragmas; | |
| 252 | ||
| 253 | if isa Trees.Definitions.FUNCTION(bare) then | |
| 254 | saw_instance_shaped = true; | |
| 255 | let function = cast Trees.Definitions.FUNCTION(bare); | |
| 256 | if !function.modifiers.is_static then | |
| 257 | return false; | |
| 258 | fi | |
| 259 | elif isa Trees.Definitions.PROPERTY(bare) then | |
| 260 | saw_instance_shaped = true; | |
| 261 | let property = cast Trees.Definitions.PROPERTY(bare); | |
| 262 | if !property.modifiers.is_static then | |
| 263 | return false; | |
| 264 | fi | |
| 265 | elif isa Trees.Variables.VARIABLE(bare) then | |
| 266 | saw_instance_shaped = true; | |
| 267 | let variable = cast Trees.Variables.VARIABLE(bare); | |
| 268 | if !variable.is_static then | |
| 269 | return false; | |
| 270 | fi | |
| 271 | fi | |
| 272 | od | |
| 273 | ||
| 274 | return saw_instance_shaped; | |
| 275 | si | |
| 276 | ||
| 277 | _warn(identifier: Trees.Identifiers.Identifier, kind: string, slug: string, case_label: string) is | |
| 278 | _logger.warn( | |
| 279 | identifier.location, | |
| 280 | slug, | |
| 281 | "non-{case_label} {kind} '{identifier.name}'" | |
| 282 | ); | |
| 283 | si | |
| 284 | ||
| 285 | _strip_backticks(name: string) -> string static is | |
| 286 | let buffer = System.Text.StringBuilder(); | |
| 287 | for i in 0..name.length do | |
| 288 | let c = name.get_chars(i); | |
| 289 | if c != '`' then | |
| 290 | buffer.append(c); | |
| 291 | fi | |
| 292 | od | |
| 293 | return buffer.to_string(); | |
| 294 | si | |
| 295 | ||
| 296 | _starts_with_identifier_char(name: string) -> bool static is | |
| 297 | let c = name.get_chars(0); | |
| 298 | return (c >= 'a' /\ c <= 'z') | |
| 299 | \/ (c >= 'A' /\ c <= 'Z') | |
| 300 | \/ c == '_'; | |
| 301 | si | |
| 302 | ||
| 303 | _is_snake_case(name: string) -> bool static is | |
| 304 | for i in 0..name.length do | |
| 305 | let c = name.get_chars(i); | |
| 306 | let is_lower = c >= 'a' /\ c <= 'z'; | |
| 307 | let is_digit = c >= '0' /\ c <= '9'; | |
| 308 | let is_underscore = c == '_'; | |
| 309 | ||
| 310 | if !is_lower /\ !is_digit /\ !is_underscore then | |
| 311 | return false; | |
| 312 | fi | |
| 313 | od | |
| 314 | ||
| 315 | return true; | |
| 316 | si | |
| 317 | ||
| 318 | // PascalCase: the first letter is upper-case, every | |
| 319 | // subsequent character is a letter or digit. Underscores | |
| 320 | // are not permitted in the body. A leading underscore is | |
| 321 | // allowed as a private-name marker (e.g. `_Helper`); the | |
| 322 | // first letter after underscores must be upper-case. | |
| 323 | _is_pascal_case(name: string) -> bool static is | |
| 324 | let i mut = 0; | |
| 325 | ||
| 326 | while i < name.length /\ name.get_chars(i) == '_' do | |
| 327 | i = i + 1; | |
| 328 | od | |
| 329 | ||
| 330 | if i >= name.length then | |
| 331 | return false; | |
| 332 | fi | |
| 333 | ||
| 334 | let first = name.get_chars(i); | |
| 335 | ||
| 336 | if !(first >= 'A' /\ first <= 'Z') then | |
| 337 | return false; | |
| 338 | fi | |
| 339 | ||
| 340 | i = i + 1; | |
| 341 | ||
| 342 | while i < name.length do | |
| 343 | let c = name.get_chars(i); | |
| 344 | let is_upper = c >= 'A' /\ c <= 'Z'; | |
| 345 | let is_lower = c >= 'a' /\ c <= 'z'; | |
| 346 | let is_digit = c >= '0' /\ c <= '9'; | |
| 347 | ||
| 348 | if !is_upper /\ !is_lower /\ !is_digit then | |
| 349 | return false; | |
| 350 | fi | |
| 351 | ||
| 352 | i = i + 1; | |
| 353 | od | |
| 354 | ||
| 355 | return true; | |
| 356 | si | |
| 357 | ||
| 358 | // UPPER_SNAKE_CASE: only upper-case letters, digits and | |
| 359 | // underscores. At least one upper-case letter is required | |
| 360 | // so a bare `_` or `_123` doesn't pass. | |
| 361 | _is_upper_snake_case(name: string) -> bool static is | |
| 362 | let saw_upper mut = false; | |
| 363 | ||
| 364 | for i in 0..name.length do | |
| 365 | let c = name.get_chars(i); | |
| 366 | let is_upper = c >= 'A' /\ c <= 'Z'; | |
| 367 | let is_digit = c >= '0' /\ c <= '9'; | |
| 368 | let is_underscore = c == '_'; | |
| 369 | ||
| 370 | if !is_upper /\ !is_digit /\ !is_underscore then | |
| 371 | return false; | |
| 372 | fi | |
| 373 | ||
| 374 | if is_upper then | |
| 375 | saw_upper = true; | |
| 376 | fi | |
| 377 | od | |
| 378 | ||
| 379 | return saw_upper; | |
| 380 | si | |
| 381 | si | |
| 382 | si |