Skip to content
← Back

src/syntax/process/check_name_conventions.ghul

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
let function = cast Trees.Definitions.FUNCTION(bare)
255
256
// What the author wrote is what says whether this is
257
// a container of statics. A member the compiler adds
258
// - an equality operator, a hash, a bridge - is not
259
// the author's instance member and must not take the
260
// class out of the exemption.
261
if function.is_synthesized then
262
continue
263
fi
264
265
saw_instance_shaped = true
266
267
if !function.modifiers.is_static then
268
return false
269
fi
270
elif isa Trees.Definitions.PROPERTY(bare) then
271
saw_instance_shaped = true
272
let property = cast Trees.Definitions.PROPERTY(bare)
273
if !property.modifiers.is_static then
274
return false
275
fi
276
elif isa Trees.Variables.VARIABLE(bare) then
277
saw_instance_shaped = true
278
let variable = cast Trees.Variables.VARIABLE(bare)
279
if !variable.is_static then
280
return false
281
fi
282
fi
283
od
284
285
return saw_instance_shaped
286
si
287
288
_warn(identifier: Trees.Identifiers.Identifier, kind: string, slug: string, case_label: string) is
289
_logger.warn(
290
identifier.location,
291
slug,
292
"non-{case_label} {kind} '{identifier.name}'"
293
)
294
si
295
296
_strip_backticks(name: string) -> string static is
297
let buffer = System.Text.StringBuilder()
298
for i in 0..name.length do
299
let c = name[i]
300
if c != '`' then
301
buffer.append(c)
302
fi
303
od
304
return buffer.to_string()
305
si
306
307
_starts_with_identifier_char(name: string) -> bool static =>
308
Lexical.TOKENIZER.is_identifier_start(name[0])
309
310
// A character with an upper and a lower form is read against
311
// the convention; one with neither, as in Chinese or Arabic,
312
// says nothing about case and so is correct wherever it sits.
313
_is_cased(c: char) -> bool static =>
314
char.is_upper(c) \/
315
char.is_lower(c) \/
316
char.get_unicode_category(c) ==
317
System.Globalization.UnicodeCategory.TITLECASE_LETTER
318
319
_is_snake_case(name: string) -> bool static is
320
for i in 0..name.length do
321
let c = name[i]
322
323
if _is_cased(c) /\ !char.is_lower(c) then
324
return false
325
fi
326
od
327
328
return true
329
si
330
331
// PascalCase: no underscore except leading ones, which mark
332
// a private name (e.g. `_Helper`). The first character after
333
// them is upper-case where it has a case.
334
_is_pascal_case(name: string) -> bool static is
335
let i mut = 0
336
337
while i < name.length /\ name[i] == '_' do
338
i = i + 1
339
od
340
341
if i >= name.length then
342
return false
343
fi
344
345
let first = name[i]
346
347
if _is_cased(first) /\ !char.is_upper(first) then
348
return false
349
fi
350
351
i = i + 1
352
353
while i < name.length do
354
if name[i] == '_' then
355
return false
356
fi
357
358
i = i + 1
359
od
360
361
return true
362
si
363
364
// UPPER_SNAKE_CASE: every cased character is upper-case, and
365
// at least one letter is present so a bare `_` or `_123`
366
// doesn't pass. An uncased letter counts towards that: a name
367
// written in a script with no case has no upper form to show.
368
_is_upper_snake_case(name: string) -> bool static is
369
let saw_letter mut = false
370
371
for i in 0..name.length do
372
let c = name[i]
373
374
if _is_cased(c) /\ !char.is_upper(c) then
375
return false
376
fi
377
378
if char.is_letter(c) then
379
saw_letter = true
380
fi
381
od
382
383
return saw_letter
384
si
385
si
386
si