linter/rules/naming-convention-rule.spice
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | // Std imports | ||
| 4 | import "std/text/analysis"; | ||
| 5 | import "std/data/vector"; | ||
| 6 | |||
| 7 | // Own imports | ||
| 8 | import "@/ast/ast-nodes"; | ||
| 9 | import "@/linter/lint-finding"; | ||
| 10 | |||
| 11 | // Flags Spice-source identifiers that violate the naming conventions documented in STYLE_GUIDE.md | ||
| 12 | // (functions/procedures/methods: camelCase; structs/interfaces/unions: PascalCase; globals: SNAKE_CASE). | ||
| 13 | // | ||
| 14 | // This is deliberately a style rule, not a semantic one: unlike unused-symbol or shadowing checks (which stay | ||
| 15 | // compiler warnings), naming conventions are opinionated and belong to the opt-in `spice lint` command rather | ||
| 16 | // than every `build`/`run`/`test` invocation. | ||
| 17 | const string NAMING_CONVENTION_RULE_ID = "naming-convention"; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Get the first char of a non-empty string | ||
| 21 | */ | ||
| 22 | 14 | f<char> firstChar(const String& input) { | |
| 23 | 14 | return input[0]; | |
| 24 | } | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Check whether an identifier is written in camelCase (starts lowercase, no underscores) | ||
| 28 | */ | ||
| 29 | 8 | f<bool> isCamelCase(const String& input) { | |
| 30 | 8 | return !input.isEmpty() && isLower(firstChar(input)) && !input.contains("_"); | |
| 31 | } | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Check whether an identifier is written in PascalCase (starts uppercase, no underscores) | ||
| 35 | */ | ||
| 36 | 6 | f<bool> isPascalCase(const String& input) { | |
| 37 | 6 | return !input.isEmpty() && isUpper(firstChar(input)) && !input.contains("_"); | |
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Check whether an identifier is written in SNAKE_CASE (all uppercase, underscores) | ||
| 42 | */ | ||
| 43 | 2 | f<bool> isSnakeCase(const String& input) { | |
| 44 | 2 | if input.isEmpty() { return false; } | |
| 45 | 2 | const unsigned long length = input.getLength(); | |
| 46 | 14 | for unsigned long i = 0l; i < length; i++ { | |
| 47 | 13 | const char c = input[i]; | |
| 48 | 13 | if !isUpper(c) && c != '_' { | |
| 49 | 1 | return false; | |
| 50 | } | ||
| 51 | 12 | } | |
| 52 | 1 | return true; | |
| 53 | } | ||
| 54 | |||
| 55 | 7 | public p checkNamingFctDef(ASTFctDefNode* node, Vector<LintFinding>& findings) { | |
| 56 | // Operator overloads (e.g. "op.plus") intentionally do not follow camelCase | ||
| 57 | 7 | if node.node.name.name.startsWith(OP_FCT_PREFIX) { return; } | |
| 58 | 6 | if !isCamelCase(node.node.name.name) { | |
| 59 | 1 | const String message = String("Function name '") + node.node.name.name + "' should be camelCase"; | |
| 60 | 1 | findings.pushBack(LintFinding(node.node.name.codeLoc, NAMING_CONVENTION_RULE_ID, LintSeverity::WARNING, message)); | |
| 61 | 1 | } | |
| 62 | 6 | } | |
| 63 | |||
| 64 | 3 | public p checkNamingProcDef(ASTProcDefNode* node, Vector<LintFinding>& findings) { | |
| 65 | // Constructors are always named "ctor" by language convention | ||
| 66 | 3 | if node.isCtor { return; } | |
| 67 | 2 | if !isCamelCase(node.node.name.name) { | |
| 68 | 1 | const String message = String("Procedure name '") + node.node.name.name + "' should be camelCase"; | |
| 69 | 1 | findings.pushBack(LintFinding(node.node.name.codeLoc, NAMING_CONVENTION_RULE_ID, LintSeverity::WARNING, message)); | |
| 70 | 1 | } | |
| 71 | 2 | } | |
| 72 | |||
| 73 | 2 | public p checkNamingStructDef(ASTStructDefNode* node, Vector<LintFinding>& findings) { | |
| 74 | 2 | if !isPascalCase(node.structName) { | |
| 75 | 1 | const String message = String("Struct name '") + node.structName + "' should be PascalCase"; | |
| 76 | 1 | findings.pushBack(LintFinding(node.codeLoc, NAMING_CONVENTION_RULE_ID, LintSeverity::WARNING, message)); | |
| 77 | 1 | } | |
| 78 | 2 | } | |
| 79 | |||
| 80 | 2 | public p checkNamingInterfaceDef(ASTInterfaceDefNode* node, Vector<LintFinding>& findings) { | |
| 81 | 2 | if !isPascalCase(node.interfaceName) { | |
| 82 | 1 | const String message = String("Interface name '") + node.interfaceName + "' should be PascalCase"; | |
| 83 | 1 | findings.pushBack(LintFinding(node.codeLoc, NAMING_CONVENTION_RULE_ID, LintSeverity::WARNING, message)); | |
| 84 | 1 | } | |
| 85 | 2 | } | |
| 86 | |||
| 87 | 2 | public p checkNamingUnionDef(ASTUnionDefNode* node, Vector<LintFinding>& findings) { | |
| 88 | 2 | if !isPascalCase(node.unionName) { | |
| 89 | 1 | const String message = String("Union name '") + node.unionName + "' should be PascalCase"; | |
| 90 | 1 | findings.pushBack(LintFinding(node.codeLoc, NAMING_CONVENTION_RULE_ID, LintSeverity::WARNING, message)); | |
| 91 | 1 | } | |
| 92 | 2 | } | |
| 93 | |||
| 94 | 2 | public p checkNamingGlobalVarDef(ASTGlobalVarDefNode* node, Vector<LintFinding>& findings) { | |
| 95 | 2 | if !isSnakeCase(node.varName) { | |
| 96 | 1 | const String message = String("Global variable name '") + node.varName + "' should be SNAKE_CASE"; | |
| 97 | 1 | findings.pushBack(LintFinding(node.codeLoc, NAMING_CONVENTION_RULE_ID, LintSeverity::WARNING, message)); | |
| 98 | 1 | } | |
| 99 | 2 | } | |
| 100 |