@blundergoat/gruff-ts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +87 -0
  3. package/LICENSE +21 -0
  4. package/README.md +303 -0
  5. package/SECURITY.md +45 -0
  6. package/bin/gruff-ts +25 -0
  7. package/docs/CONFIGURATION.md +220 -0
  8. package/docs/RELEASING.md +103 -0
  9. package/docs/REPORTS_AND_CI.md +156 -0
  10. package/fixtures/sample.ts +21 -0
  11. package/package.json +56 -0
  12. package/scripts/bump-version.sh +145 -0
  13. package/scripts/check.sh +4 -0
  14. package/scripts/npm-publish.sh +258 -0
  15. package/scripts/preflight-checks.sh +357 -0
  16. package/scripts/start-dev.sh +8 -0
  17. package/scripts/test-performance.sh +695 -0
  18. package/src/analyser.ts +461 -0
  19. package/src/baseline.ts +90 -0
  20. package/src/blocks.ts +687 -0
  21. package/src/class-rules.ts +326 -0
  22. package/src/cli-program.ts +326 -0
  23. package/src/cli.ts +19 -0
  24. package/src/comment-rules.ts +605 -0
  25. package/src/comment-scanner.ts +357 -0
  26. package/src/config.ts +622 -0
  27. package/src/constants.ts +4 -0
  28. package/src/context-doc-rules.ts +241 -0
  29. package/src/dashboard.ts +114 -0
  30. package/src/dead-code-rules.ts +183 -0
  31. package/src/discovery.ts +508 -0
  32. package/src/doc-rules.ts +368 -0
  33. package/src/findings-helpers.ts +108 -0
  34. package/src/findings.ts +45 -0
  35. package/src/fixture-purpose-rules.ts +334 -0
  36. package/src/fixtures/rule-catalogue-security-doctrine.ts +132 -0
  37. package/src/github-actions-rules.ts +413 -0
  38. package/src/line-rules.ts +538 -0
  39. package/src/naming-pushers.ts +191 -0
  40. package/src/project-config-rules.ts +555 -0
  41. package/src/project-rules.ts +545 -0
  42. package/src/report-renderers.ts +691 -0
  43. package/src/rule-list.ts +179 -0
  44. package/src/rules.ts +135 -0
  45. package/src/safety-rules.ts +355 -0
  46. package/src/scoring.ts +74 -0
  47. package/src/security-flow-rules.ts +112 -0
  48. package/src/sensitive-data-rules.ts +288 -0
  49. package/src/source-text.ts +722 -0
  50. package/src/test-block-rules.ts +347 -0
  51. package/src/test-fixtures.ts +621 -0
  52. package/src/text-scans.ts +193 -0
  53. package/src/types.ts +113 -0
  54. package/tsconfig.json +15 -0
@@ -0,0 +1,191 @@
1
+ // Shared "push a naming finding at this line" helpers. Used both by the per-line walker in
2
+ // line-rules.ts and by the parameter / interface-field walkers in cli.ts and class-rules.ts. Stable
3
+ // metadata.surface distinguishes call sites while every finding uses the same fingerprint identity.
4
+ import { type SourceFile } from "./discovery.ts";
5
+ import { makeFinding } from "./findings.ts";
6
+ import { escapeRegex } from "./findings-helpers.ts";
7
+ import type { Config, Finding } from "./types.ts";
8
+
9
+ /**
10
+ * Stable label for the call-site that emitted a naming finding. Surfaces in `metadata.surface`
11
+ * so downstream tooling can split per-pillar reports by origin without re-running the analyser.
12
+ */
13
+ export type NamingSurface = "declaration" | "parameter" | "destructure" | "interface-field";
14
+
15
+ /*
16
+ * Negative-framed booleans (disableX, noX, preventX, …) read as double negations at call sites.
17
+ * `negativeBooleanAllowed` is the user-curated exemption list. Reports the stable
18
+ * `naming.negative-boolean` finding.
19
+ */
20
+ export function pushNegativeBooleanAt(file: SourceFile, line: number, name: string, config: Config, findings: Finding[], surface: NamingSurface): void {
21
+ if (!/^(?:disable|no|not|prevent|skip|disallow)[A-Z]/.test(name)) {
22
+ return;
23
+ }
24
+ if (config.negativeBooleanAllowed.has(name.toLowerCase())) {
25
+ return;
26
+ }
27
+ findings.push(
28
+ makeFinding({
29
+ ruleId: "naming.negative-boolean",
30
+ message: `Boolean identifier \`${name}\` is framed as a negation.`,
31
+ filePath: file.displayPath,
32
+ line,
33
+ severity: "advisory",
34
+ pillar: "naming",
35
+ confidence: "medium",
36
+ symbol: name,
37
+ remediation: "Invert the framing so callers do not read a double negation.",
38
+ metadata: { identifierName: name, surface },
39
+ }),
40
+ );
41
+ }
42
+
43
+ /*
44
+ * Booleans should announce their boolean-ness with an `is`/`has`/`can`/… prefix. The accepted set
45
+ * lives in `config.booleanPrefixes` so projects can tune it. Reports the stable
46
+ * `naming.boolean-prefix` finding.
47
+ */
48
+ export function pushBooleanPrefixAt(file: SourceFile, line: number, name: string, config: Config, findings: Finding[], surface: NamingSurface): void {
49
+ if (hasBooleanPrefix(name, config.booleanPrefixes) || isAcceptedBooleanStateName(name)) {
50
+ return;
51
+ }
52
+ findings.push(
53
+ makeFinding({
54
+ ruleId: "naming.boolean-prefix",
55
+ message: `Boolean identifier \`${name}\` should use an intent-revealing prefix.`,
56
+ filePath: file.displayPath,
57
+ line,
58
+ severity: "advisory",
59
+ pillar: "naming",
60
+ confidence: "medium",
61
+ symbol: name,
62
+ remediation: "Use a prefix such as is, has, can, should, or will.",
63
+ metadata: { identifierName: name, surface },
64
+ }),
65
+ );
66
+ }
67
+
68
+ const ACCEPTED_BOOLEAN_STATE_NAMES = new Set(["acknowledged", "exists", "validated", "detected", "resolved", "selected", "installed"]);
69
+ const ACCEPTED_BOOLEAN_STATE_SUFFIXES = ["Available", "Required", "Validated", "Detected", "Resolved", "Selected", "Installed"];
70
+
71
+ // Some booleans are state adjectives rather than predicate phrases. Keep this list deliberately
72
+ // narrow so vague names such as `ready` or `enabled` still get the maintainability prompt.
73
+ function isAcceptedBooleanStateName(name: string): boolean {
74
+ return ACCEPTED_BOOLEAN_STATE_NAMES.has(name.toLowerCase()) || ACCEPTED_BOOLEAN_STATE_SUFFIXES.some((suffix) => name.endsWith(suffix));
75
+ }
76
+
77
+ /*
78
+ * Allows the standard `i`, `j`, `k` loop counters and anything on `acceptedAbbreviations`. Reports
79
+ * `naming.short-variable` for any other one or two character name as a stable advisory finding.
80
+ */
81
+ export function pushShortVariableAt(file: SourceFile, line: number, name: string, config: Config, findings: Finding[], surface: NamingSurface): void {
82
+ if (name.length > 2 || ["i", "j", "k"].includes(name) || config.acceptedAbbreviations.has(name.toLowerCase())) {
83
+ return;
84
+ }
85
+ findings.push(
86
+ makeFinding({
87
+ ruleId: "naming.short-variable",
88
+ message: `Variable \`${name}\` is too short to explain intent.`,
89
+ filePath: file.displayPath,
90
+ line,
91
+ severity: "advisory",
92
+ pillar: "naming",
93
+ confidence: "medium",
94
+ symbol: name,
95
+ remediation: "Use a name that describes the domain role.",
96
+ metadata: { surface },
97
+ }),
98
+ );
99
+ }
100
+
101
+ /*
102
+ * Reports `naming.identifier-quality` when a name resolves to a generic variant via
103
+ * `identifierQualityVariant` (placeholder names from config, or built-in low-info forms like `data`).
104
+ * The stable `variant` metadata lets downstream tools group by category.
105
+ */
106
+ export function pushIdentifierQualityAt(file: SourceFile, line: number, name: string, config: Config, findings: Finding[], surface: NamingSurface): void {
107
+ const variant = identifierQualityVariant(name, config.placeholderNames);
108
+ if (!variant) {
109
+ return;
110
+ }
111
+ findings.push(
112
+ makeFinding({
113
+ ruleId: "naming.identifier-quality",
114
+ message: `Identifier \`${name}\` is a ${variant} name that does not explain domain intent.`,
115
+ filePath: file.displayPath,
116
+ line,
117
+ severity: "advisory",
118
+ pillar: "naming",
119
+ confidence: "medium",
120
+ symbol: name,
121
+ remediation: "Use an identifier that names the domain role.",
122
+ metadata: { identifierName: name, variant, surface },
123
+ }),
124
+ );
125
+ }
126
+
127
+ /*
128
+ * Reports `naming.abbreviation` when the name is on `abbreviationDenylist` and not on the user's
129
+ * `acceptedAbbreviations` allowlist. `surface` distinguishes parameter / variable / interface-field
130
+ * - same stable rule contract, different metadata, so consumers can filter on origin.
131
+ */
132
+ export function pushAbbreviationAt(file: SourceFile, line: number, name: string, config: Config, findings: Finding[], surface: NamingSurface): void {
133
+ if (config.rules.get("naming.abbreviation")?.enabled !== true) {
134
+ return;
135
+ }
136
+ if (config.acceptedAbbreviations.has(name.toLowerCase())) {
137
+ return;
138
+ }
139
+ if (!config.abbreviationDenylist.has(name.toLowerCase())) {
140
+ return;
141
+ }
142
+ findings.push(
143
+ makeFinding({
144
+ ruleId: "naming.abbreviation",
145
+ message: `Identifier \`${name}\` uses an opaque abbreviation.`,
146
+ filePath: file.displayPath,
147
+ line,
148
+ severity: "advisory",
149
+ pillar: "naming",
150
+ confidence: "medium",
151
+ symbol: name,
152
+ remediation: "Use the full domain term or add the abbreviation to allowlists.acceptedAbbreviations.",
153
+ metadata: { identifierName: name, surface },
154
+ }),
155
+ );
156
+ }
157
+
158
+ // Returns `"generic"` for low-information names from the configured set, `"numbered"` for
159
+ // `foo1` / `bar2` style trailing-digit identifiers, or undefined when the name is acceptable.
160
+ // The variant string lands in finding metadata so consumers can split the two failure modes.
161
+ function identifierQualityVariant(name: string, placeholderNames: Set<string>): string | undefined {
162
+ if (placeholderNames.has(name.toLowerCase())) {
163
+ return "generic";
164
+ }
165
+ if (/^[A-Za-z_$]+[0-9]+$/.test(name)) {
166
+ return "numbered";
167
+ }
168
+ return undefined;
169
+ }
170
+
171
+ const BOOLEAN_PREFIX_REGEX_CACHE = new WeakMap<Set<string>, RegExp | null>();
172
+
173
+ // Tests the cached prefix regex from `booleanPrefixRegex`. A null regex (empty prefix set) is
174
+ // treated as "no rule configured" so the boolean-prefix check fires only when configured.
175
+ function hasBooleanPrefix(name: string, prefixes: Set<string>): boolean {
176
+ const regex = booleanPrefixRegex(prefixes);
177
+ return regex !== null && regex.test(name);
178
+ }
179
+
180
+ // Cached per prefix Set via a WeakMap so each rule pass reuses the compiled regex instead of
181
+ // rebuilding it for every identifier. The trailing `[A-Z_]` requirement keeps single-letter
182
+ // names like `is` from falsely matching the prefix-followed-by-name pattern.
183
+ function booleanPrefixRegex(prefixes: Set<string>): RegExp | null {
184
+ if (BOOLEAN_PREFIX_REGEX_CACHE.has(prefixes)) {
185
+ return BOOLEAN_PREFIX_REGEX_CACHE.get(prefixes) ?? null;
186
+ }
187
+ const escaped = [...prefixes].map(escapeRegex);
188
+ const regex = prefixes.size === 0 ? null : new RegExp(`^(?:${escaped.join("|")})[A-Z_]`);
189
+ BOOLEAN_PREFIX_REGEX_CACHE.set(prefixes, regex);
190
+ return regex;
191
+ }