@rigour-labs/core 6.10.0-rc.2 → 6.10.1-rc.1

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.
@@ -23,7 +23,7 @@ export class ContextGate extends Gate {
23
23
  return [];
24
24
  const files = await FileScanner.findFiles({
25
25
  cwd: context.cwd,
26
- patterns: ['**/*.{ts,js,tsx,jsx,py,go,java,cs,rb,kt,swift,rs,php}'],
26
+ patterns: ['**/*.{ts,js,tsx,jsx,py,go,java,cs,rb,kt,scala,swift,rs,php}'],
27
27
  ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
28
28
  '**/studio-dist/**', '**/.next/**', '**/coverage/**', '**/out/**',
29
29
  '**/*.test.*', '**/*.spec.*', '**/examples/**', '**/docs/**'],
@@ -93,10 +93,10 @@ export class ContextGate extends Gate {
93
93
  const casing = this.detectCasing(match[1]);
94
94
  this.addPattern(patterns, 'function', { casing, file, count: 1 });
95
95
  }
96
- // Class declarations: `class` opening a line (after modifiers: C# partial, Kotlin enum/inner/value/annotation
97
- // and the rest), its name on the same line. Not the end of
96
+ // Class declarations: `class` opening a line (after modifiers: C# partial, Kotlin enum/inner/value/annotation,
97
+ // Scala case/implicit and a qualified `private[pkg]`, and the rest), its name on the same line. Not the end of
98
98
  // `dataclass`, and not the word in a comment or a string ("# this class of errors").
99
- const classMatches = content.matchAll(/^[ \t]*(?:(?:export|default|abstract|public|private|protected|internal|final|sealed|static|data|open|partial|enum|inner|value|annotation)[ \t]+)*class[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)/gm);
99
+ const classMatches = content.matchAll(/^[ \t]*(?:(?:export|default|abstract|public|private|protected|internal|final|sealed|static|data|open|partial|enum|inner|value|annotation|case|implicit)(?:\[[\w.]+\])?[ \t]+)*class[ \t]+([A-Za-z_$][A-Za-z0-9_$]*)/gm);
100
100
  for (const match of classMatches) {
101
101
  const casing = this.detectCasing(match[1]);
102
102
  this.addPattern(patterns, 'class', { casing, file, count: 1 });
@@ -5,7 +5,8 @@
5
5
  * equals on `col` (SQL excludes NULL there);
6
6
  * nullable-filtered-column the query filters `col` non-null, the row type still says `| null`,
7
7
  * so every guard on it is dead;
8
- * optional-always-supplied an optional property every host object supplies;
8
+ * optional-always-supplied an optional property every host object supplies (unless values of the type are
9
+ * also read back from JSON, where data written before the member existed lacks it);
9
10
  * write-only-property a property the hosts set and nothing reads. When the value only
10
11
  * leaves through serialisation to a callee the program does not declare
11
12
  * (a wire payload), that is a hint for the reviewer, not a block.
@@ -263,6 +264,53 @@ function redundancyFailures(typed, changedLines, config) {
263
264
  escapeMemo.set(t.sym, found);
264
265
  return found;
265
266
  };
267
+ // Values of the type read back from JSON (JSON.parse, readJson, a response's .json()) as the type: an assertion,
268
+ // an annotated variable, a type argument, or the declared return of the function that returns them. Data written
269
+ // before a member existed lacks it, so the member stays optional however every host in the code supplies it now.
270
+ const holdsType = (t, ty) => {
271
+ if (!ty)
272
+ return false;
273
+ const awaited = checker.getAwaitedType(ty) ?? ty;
274
+ return mentionsType(t, awaited) || checker.getIndexInfosOfType(awaited).some(info => mentionsType(t, info.type));
275
+ };
276
+ const JSON_READ = /(?:^|\.)(?:JSON\.parse|readJson|readJsonSync|json)$/;
277
+ const persistedMemo = new Map();
278
+ const readFromJson = (t) => {
279
+ if (persistedMemo.has(t.sym))
280
+ return persistedMemo.get(t.sym);
281
+ let found;
282
+ for (const sf of typed.appSources) {
283
+ if (found)
284
+ break;
285
+ if (!/JSON\.parse|readJson|\.json\(/.test(sf.text))
286
+ continue;
287
+ walk(sf, node => {
288
+ if (found || !ts.isCallExpression(node) || !JSON_READ.test(node.expression.getText()))
289
+ return;
290
+ if (node.typeArguments?.some(a => holdsType(t, checker.getTypeFromTypeNode(a)))) {
291
+ found = at(node);
292
+ return;
293
+ }
294
+ let site = node;
295
+ while (ts.isParenthesizedExpression(site.parent) || ts.isAwaitExpression(site.parent) || ts.isNonNullExpression(site.parent))
296
+ site = site.parent;
297
+ const p = site.parent;
298
+ const target = ts.isAsExpression(p) || ts.isTypeAssertionExpression(p) || ts.isSatisfiesExpression(p) ? p.type
299
+ : ts.isVariableDeclaration(p) && p.initializer === site ? p.type
300
+ : undefined;
301
+ if (target && holdsType(t, checker.getTypeFromTypeNode(target))) {
302
+ found = at(node);
303
+ return;
304
+ }
305
+ const fn = ts.isReturnStatement(p) ? ts.findAncestor(p, ts.isFunctionLike) : ts.isArrowFunction(p) && p.body === site ? p : undefined;
306
+ const signature = fn && checker.getSignatureFromDeclaration(fn);
307
+ if (fn?.type && signature && holdsType(t, checker.getReturnTypeOfSignature(signature)))
308
+ found = at(node);
309
+ });
310
+ }
311
+ persistedMemo.set(t.sym, found);
312
+ return found;
313
+ };
266
314
  // A property is read when any non-test file accesses or destructures it; a spread only passes it on.
267
315
  const isWriteTarget = (access) => ts.isBinaryExpression(access.parent) && access.parent.left === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
268
316
  const isRead = (t, name) => {
@@ -317,7 +365,10 @@ function redundancyFailures(typed, changedLines, config) {
317
365
  const all = hosts.length + outside.length;
318
366
  if (all > 0 && supplied.length === hosts.length && outside.every(s => supplyRe(name).test(s.text))) {
319
367
  const text = `${member} is optional but every host supplies it (${all} host(s), e.g. ${hosts[0] ? at(hosts[0]) : outside[0].file}).`;
320
- if (published)
368
+ const stored = readFromJson(t);
369
+ if (stored)
370
+ hints.push(`optional-always-supplied ${at(m)}: ${text} Values of it are read back from JSON at ${stored}, where data written before the member existed lacks it: keep it optional unless that data is migrated`);
371
+ else if (published)
321
372
  publish('optional-always-supplied', m, text);
322
373
  else
323
374
  report('optional-always-supplied', 'Optional member every host supplies', anchor, text, 'Make it required and drop the `?.` and the fallbacks on it.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.10.0-rc.2",
3
+ "version": "6.10.1-rc.1",
4
4
  "description": "Rigour's review engine: deterministic gates on changed lines, rules and lessons learned from your team's fixes, and per-check precision from what you fix versus dismiss, across TypeScript, JavaScript, Python, Go, Ruby and C#.",
5
5
  "engines": {
6
6
  "node": ">=22.13"
@@ -75,11 +75,11 @@
75
75
  "@anthropic-ai/sdk": "^0.132.1",
76
76
  "pg": "^8.16.3",
77
77
  "openai": "^5.23.2",
78
- "@rigour-labs/brain-darwin-arm64": "6.10.0-rc.2",
79
- "@rigour-labs/brain-darwin-x64": "6.10.0-rc.2",
80
- "@rigour-labs/brain-linux-x64": "6.10.0-rc.2",
81
- "@rigour-labs/brain-linux-arm64": "6.10.0-rc.2",
82
- "@rigour-labs/brain-win-x64": "6.10.0-rc.2"
78
+ "@rigour-labs/brain-darwin-arm64": "6.10.1-rc.1",
79
+ "@rigour-labs/brain-darwin-x64": "6.10.1-rc.1",
80
+ "@rigour-labs/brain-linux-arm64": "6.10.1-rc.1",
81
+ "@rigour-labs/brain-linux-x64": "6.10.1-rc.1",
82
+ "@rigour-labs/brain-win-x64": "6.10.1-rc.1"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/fs-extra": "^11.0.4",