@rcrsr/rill-cli 0.18.0 → 0.18.2

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.
@@ -59,14 +59,31 @@ export const CLOSURE_BARE_DOLLAR = {
59
59
  */
60
60
  function containsBareReference(node) {
61
61
  let found = false;
62
+ let scopeDepth = 0;
63
+ const scopeTypes = new Set([
64
+ 'Closure',
65
+ 'FilterExpr',
66
+ 'MapExpr',
67
+ 'EachExpr',
68
+ ]);
62
69
  const ctx = {};
63
70
  visitNode(node, ctx, {
64
71
  enter(n) {
72
+ if (scopeTypes.has(n.type)) {
73
+ scopeDepth++;
74
+ return;
75
+ }
76
+ if (scopeDepth > 0)
77
+ return;
65
78
  if (n.type === 'Variable' && n.isPipeVar) {
66
79
  found = true;
67
80
  }
68
81
  },
69
- exit() { },
82
+ exit(n) {
83
+ if (scopeTypes.has(n.type)) {
84
+ scopeDepth--;
85
+ }
86
+ },
70
87
  });
71
88
  return found;
72
89
  }
@@ -168,11 +185,15 @@ export const CLOSURE_LATE_BINDING = {
168
185
  validate(node, context) {
169
186
  const eachNode = node;
170
187
  const body = eachNode.body;
188
+ // For named parameter closures (|entry| { ... }), the body itself is a
189
+ // Closure. Look inside the inner block for nested closure creations,
190
+ // not the body closure itself.
191
+ const innerBody = body.type === 'Closure' ? body.body : body;
171
192
  // Check if body contains a closure creation
172
- const hasClosureCreation = containsClosureCreation(body);
193
+ const hasClosureCreation = containsClosureCreation(innerBody);
173
194
  if (hasClosureCreation) {
174
195
  // Check if there's an explicit capture before the closure
175
- const hasExplicitCapture = containsExplicitCapture(body);
196
+ const hasExplicitCapture = containsExplicitCapture(innerBody);
176
197
  if (!hasExplicitCapture) {
177
198
  return [
178
199
  {
@@ -24,6 +24,23 @@ function containsBreak(node) {
24
24
  });
25
25
  return found;
26
26
  }
27
+ /**
28
+ * Check if an AST subtree contains side-effecting operations.
29
+ * Detects HostCall (log, host functions) and ClosureCall ($fn(), $obj.method()).
30
+ */
31
+ function containsSideEffects(node) {
32
+ let found = false;
33
+ const ctx = {};
34
+ visitNode(node, ctx, {
35
+ enter(n) {
36
+ if (n.type === 'HostCall' || n.type === 'ClosureCall') {
37
+ found = true;
38
+ }
39
+ },
40
+ exit() { },
41
+ });
42
+ return found;
43
+ }
27
44
  /**
28
45
  * Check if a body is a simple method shorthand.
29
46
  * Body structure for .method shorthand is PostfixExpr with MethodCall as primary.
@@ -179,9 +196,13 @@ export const PREFER_MAP = {
179
196
  return [];
180
197
  }
181
198
  }
182
- // Simple heuristic: if body is pure (no side effects), suggest map
183
- // For now, suggest map for simple transformations
184
- // Full implementation would check for host calls, logging, etc.
199
+ // Check for side effects: host calls (log, etc.) and closure calls ($fn())
200
+ const innerBody = eachExpr.body.type === 'Closure'
201
+ ? eachExpr.body.body
202
+ : eachExpr.body;
203
+ if (containsSideEffects(innerBody)) {
204
+ return [];
205
+ }
185
206
  return [
186
207
  {
187
208
  location: node.span.start,
@@ -189,7 +210,7 @@ export const PREFER_MAP = {
189
210
  code: 'PREFER_MAP',
190
211
  message: "Consider using 'map' instead of 'each' for pure transformations (no side effects)",
191
212
  context: extractContextLine(node.span.start.line, context.source),
192
- fix: null, // Could generate fix by replacing 'each' with 'map'
213
+ fix: null,
193
214
  },
194
215
  ];
195
216
  },
@@ -125,7 +125,6 @@ export const SPACING_BRACES = {
125
125
  const span = node.span;
126
126
  const lines = context.source.split('\n');
127
127
  const openLine = lines[span.start.line - 1] ?? '';
128
- const closeLine = lines[span.end.line - 1] ?? '';
129
128
  // Check for opening brace without space after
130
129
  // Only examine the opening line (from the { onward)
131
130
  // Use ^ anchor to only check the block's opening brace, not string interpolation
@@ -141,11 +140,16 @@ export const SPACING_BRACES = {
141
140
  });
142
141
  }
143
142
  // Check for closing brace without space before
144
- // span.end.column is 1-indexed and points AFTER the }, so:
145
- // - } is at 0-index: span.end.column - 2
146
- // - Character before } is at 0-index: span.end.column - 3
147
- const charBeforeClose = closeLine[span.end.column - 3];
148
- const isCloseOnOwnLine = /^\s*$/.test(closeLine.substring(0, span.end.column - 2));
143
+ // For Closure nodes with return type annotations, span.end extends past }
144
+ // to include the type annotation. Use body.span.end to find the actual }.
145
+ const closeSpan = node.type === 'Closure' ? node.body.span : span;
146
+ const closeEnd = closeSpan.end;
147
+ const closeLineActual = lines[closeEnd.line - 1] ?? '';
148
+ // closeEnd.column is 1-indexed and points AFTER the }, so:
149
+ // - } is at 0-index: closeEnd.column - 2
150
+ // - Character before } is at 0-index: closeEnd.column - 3
151
+ const charBeforeClose = closeLineActual[closeEnd.column - 3];
152
+ const isCloseOnOwnLine = /^\s*$/.test(closeLineActual.substring(0, closeEnd.column - 2));
149
153
  if (charBeforeClose && !/\s/.test(charBeforeClose) && !isCloseOnOwnLine) {
150
154
  diagnostics.push({
151
155
  location: span.end,
package/dist/cli-run.js CHANGED
@@ -11,7 +11,7 @@ import { parse, execute, createRuntimeContext, invokeCallable, isScriptCallable,
11
11
  import { resolveConfigPath, loadProject, parseMainField, introspectHandler, marshalCliArgs, ConfigError, } from '@rcrsr/rill-config';
12
12
  import { CLI_VERSION } from './cli-shared.js';
13
13
  import { explainError } from './cli-explain.js';
14
- import { runScript } from './run/runner.js';
14
+ import { formatOutput, runScript } from './run/runner.js';
15
15
  // ============================================================
16
16
  // HELP TEXT
17
17
  // ============================================================
@@ -269,9 +269,16 @@ export async function main() {
269
269
  throw err;
270
270
  }
271
271
  ctx.pipeValue = handlerArgs;
272
+ // Map handler args to positional args in param order so marshalArgs
273
+ // can bind them to the closure's declared parameters.
274
+ // Omitted optional params stay undefined so closure defaults hydrate.
275
+ // pipeValue is kept for zero-param closures that access $ directly.
276
+ const positionalArgs = introspection.params.map((p) => (Object.prototype.hasOwnProperty.call(handlerArgs, p.name)
277
+ ? handlerArgs[p.name]
278
+ : undefined));
272
279
  let handlerResult;
273
280
  try {
274
- handlerResult = await invokeCallable(handlerValue, [], ctx);
281
+ handlerResult = await invokeCallable(handlerValue, positionalArgs, ctx);
275
282
  }
276
283
  catch (err) {
277
284
  const message = err instanceof Error ? err.message : String(err);
@@ -288,10 +295,13 @@ export async function main() {
288
295
  }
289
296
  }
290
297
  }
291
- if (typeof handlerResult === 'string' && handlerResult.length > 0) {
292
- process.stdout.write(handlerResult + '\n');
298
+ if (handlerResult !== false &&
299
+ handlerResult !== '' &&
300
+ handlerResult !== undefined) {
301
+ const output = formatOutput(handlerResult, opts.format);
302
+ process.stdout.write(output + '\n');
293
303
  }
294
- process.exit(0);
304
+ process.exit(handlerResult === false || handlerResult === '' ? 1 : 0);
295
305
  }
296
306
  // Module mode: main field in config is required
297
307
  if (mainField === undefined) {
@@ -17,6 +17,7 @@ export interface RunResult {
17
17
  * - `module:alias` → `{dir}/index.rill`
18
18
  */
19
19
  export declare function buildModuleResolver(modulesConfig: Record<string, string>, configDir: string): SchemeResolver;
20
+ export declare function formatOutput(value: RillValue, format: RunCliOptions['format']): string;
20
21
  /**
21
22
  * Run a rill script file with the given extension tree and config.
22
23
  */
@@ -59,7 +59,7 @@ function mapResultToRunResult(result, format) {
59
59
  const formatted = formatOutput(result, format);
60
60
  return { exitCode: 0, output: formatted };
61
61
  }
62
- function formatOutput(value, format) {
62
+ export function formatOutput(value, format) {
63
63
  const native = toNative(value);
64
64
  if (format === 'json' || format === 'compact') {
65
65
  return JSON.stringify(native.value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcrsr/rill-cli",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "CLI tools for the rill scripting language",
5
5
  "license": "MIT",
6
6
  "author": "Andre Bremer",
@@ -21,8 +21,8 @@
21
21
  "dependencies": {
22
22
  "dotenv": "^16.0.0",
23
23
  "yaml": "^2.8.2",
24
- "@rcrsr/rill": "^0.18.0",
25
- "@rcrsr/rill-config": "^0.18.0"
24
+ "@rcrsr/rill": "^0.18.2",
25
+ "@rcrsr/rill-config": "^0.18.1"
26
26
  },
27
27
  "files": [
28
28
  "dist"