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
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