opencode-leetcode-realworld 0.2.3 → 0.3.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.
package/README.md CHANGED
@@ -182,6 +182,43 @@ node tests/runner.mjs # all cases
182
182
  node tests/runner.mjs --public # visible cases only
183
183
  ```
184
184
 
185
+ ## Typed starters
186
+
187
+ The starter file is generated from your test cases, so the input/output contract
188
+ is explicit before you write any logic:
189
+
190
+ - **Type-safe languages** (`typescript`, `go`, `rust`, `csharp`, `swift`) get real
191
+ type declarations inferred from the sample inputs/outputs — an interface /
192
+ struct / class per object, with nested objects and arrays typed too — plus a
193
+ typed function signature. Your editor autocompletes `payload.<field>`.
194
+ - **Dynamic languages** (`javascript`, `python`, `ruby`, `php`) — and Swift when a
195
+ shape can't be expressed statically — get a commented input/output format with a
196
+ concrete example instead.
197
+
198
+ Example (TypeScript), generated from input `{ "values": [1,2,3] }` -> output `[3,2,1]`:
199
+
200
+ ```ts
201
+ interface Payload {
202
+ values: number[];
203
+ }
204
+
205
+ function solve(payload: Payload): number[] {
206
+ throw new Error("Not implemented: solve");
207
+ }
208
+ ```
209
+
210
+ Example (Python):
211
+
212
+ ```python
213
+ # Input format: { values: number[] }
214
+ # Input example: {"values":[1,2,3]}
215
+ # Output format: number[]
216
+ # Output example: [3,2,1]
217
+
218
+ def solve(payload):
219
+ raise NotImplementedError("Not implemented: solve")
220
+ ```
221
+
185
222
  ## Running from source
186
223
 
187
224
  For plugin development, clone the repo and open opencode inside it — files under
@@ -234,6 +271,7 @@ src/
234
271
  index.ts plugin entry: tools, guard hooks, command registration
235
272
  leetcode.ts API client + normalizer (handles endpoint shape differences)
236
273
  languages.ts per-language adapters: starter, build, run commands
274
+ schema.ts input/output schema inference + type generation
237
275
  patterns.ts topic -> domain seeds, leak detection
238
276
  scaffold.ts deterministic project writer (any supported language)
239
277
  guard.ts protected-path detection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-leetcode-realworld",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "OpenCode plugin that turns a LeetCode problem into a realistic software-engineering assignment, so you practice applying the underlying algorithm instead of solving the puzzle.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/languages.ts CHANGED
@@ -1,9 +1,25 @@
1
+ import {
2
+ buildTypes,
3
+ isFullyTyped,
4
+ isIdentifier,
5
+ isReserved,
6
+ pascal,
7
+ schemaSignature,
8
+ snake,
9
+ type SchemaNode,
10
+ type TypeDecl,
11
+ type TypeRef,
12
+ } from "./schema";
1
13
  import type { FileEntry, LanguageId } from "./types";
2
14
 
3
15
  export interface LanguageContext {
4
16
  projectName: string;
5
17
  functionName: string;
6
18
  title: string;
19
+ input: SchemaNode;
20
+ output: SchemaNode;
21
+ inputExample: unknown;
22
+ outputExample: unknown;
7
23
  }
8
24
 
9
25
  export interface LanguageAdapter {
@@ -11,17 +27,32 @@ export interface LanguageAdapter {
11
27
  name: string;
12
28
  aliases: string[];
13
29
  extension: string;
14
- /** Human hint shown in the generated README. */
15
30
  runtime: string;
31
+ /** True when the starter exposes static types for the payload/result. */
32
+ typed: boolean;
16
33
  starter(ctx: LanguageContext): string;
17
- /** Extra project files (build manifests etc). */
18
34
  files(ctx: LanguageContext): FileEntry[];
19
- /** Command run once before the cases, or null when not needed. */
20
35
  build(ctx: LanguageContext): string | null;
21
- /** Command that runs the solution once, reading one JSON value on stdin. */
22
36
  run(ctx: LanguageContext): string;
23
37
  }
24
38
 
39
+ const INPUT_ROOT = "Payload";
40
+ const OUTPUT_ROOT = "Output";
41
+
42
+ function example(value: unknown): string {
43
+ return JSON.stringify(value);
44
+ }
45
+
46
+ /** Format comment for languages without static types (and as a fallback). */
47
+ function formatComment(prefix: string, ctx: LanguageContext): string {
48
+ return [
49
+ `${prefix} Input format: ${schemaSignature(ctx.input)}`,
50
+ `${prefix} Input example: ${example(ctx.inputExample)}`,
51
+ `${prefix} Output format: ${schemaSignature(ctx.output)}`,
52
+ `${prefix} Output example: ${example(ctx.outputExample)}`,
53
+ ].join("\n");
54
+ }
55
+
25
56
  function packageJson(projectName: string, script: string): FileEntry {
26
57
  return {
27
58
  relative: "package.json",
@@ -40,6 +71,401 @@ function packageJson(projectName: string, script: string): FileEntry {
40
71
  };
41
72
  }
42
73
 
74
+ /* ------------------------------------------------------------------ */
75
+ /* TypeScript: named interfaces -> dot completion on `payload.` */
76
+ /* ------------------------------------------------------------------ */
77
+
78
+ function tsRef(ref: TypeRef): string {
79
+ switch (ref.t) {
80
+ case "string":
81
+ return "string";
82
+ case "int":
83
+ case "double":
84
+ return "number";
85
+ case "bool":
86
+ return "boolean";
87
+ case "any":
88
+ return "unknown";
89
+ case "null":
90
+ return "null";
91
+ case "array": {
92
+ const inner = tsRef(ref.of);
93
+ return inner.includes(" ") ? `(${inner})[]` : `${inner}[]`;
94
+ }
95
+ case "named":
96
+ return ref.name;
97
+ }
98
+ }
99
+
100
+ function tsProp(name: string): string {
101
+ return isIdentifier(name) && !isReserved(name) ? name : JSON.stringify(name);
102
+ }
103
+
104
+ function tsDecl(decl: TypeDecl): string {
105
+ const fields = decl.fields
106
+ .map((field) => ` ${tsProp(field.name)}${field.optional ? "?" : ""}: ${tsRef(field.ref)};`)
107
+ .join("\n");
108
+ return `interface ${decl.name} {\n${fields}\n}`;
109
+ }
110
+
111
+ function typescriptStarter(ctx: LanguageContext): string {
112
+ const input = buildTypes(INPUT_ROOT, ctx.input);
113
+ const output = buildTypes(OUTPUT_ROOT, ctx.output);
114
+ const decls = [...input.decls, ...output.decls].map(tsDecl).join("\n\n");
115
+ const inputType = tsRef(input.root);
116
+ const outputType = tsRef(output.root);
117
+ const header = decls ? `${decls}\n\n` : "";
118
+ return `import { readFileSync } from "node:fs";
119
+
120
+ ${header}function ${ctx.functionName}(payload: ${inputType}): ${outputType} {
121
+ throw new Error("Not implemented: ${ctx.functionName}");
122
+ }
123
+
124
+ const input = readFileSync(0, "utf8");
125
+ const payload = JSON.parse(input.trim() === "" ? "null" : input) as ${inputType};
126
+ process.stdout.write(JSON.stringify(${ctx.functionName}(payload) ?? null));
127
+ `;
128
+ }
129
+
130
+ /* ------------------------------------------------------------------ */
131
+ /* Go: structs with json tags */
132
+ /* ------------------------------------------------------------------ */
133
+
134
+ function goRef(ref: TypeRef): string {
135
+ switch (ref.t) {
136
+ case "string":
137
+ return "string";
138
+ case "int":
139
+ return "int";
140
+ case "double":
141
+ return "float64";
142
+ case "bool":
143
+ return "bool";
144
+ case "any":
145
+ case "null":
146
+ return "any";
147
+ case "array":
148
+ return "[]" + goRef(ref.of);
149
+ case "named":
150
+ return ref.name;
151
+ }
152
+ }
153
+
154
+ function goFieldName(name: string): string {
155
+ const value = pascal(name);
156
+ return value.length > 0 ? value : "Field";
157
+ }
158
+
159
+ function goDecl(decl: TypeDecl): string {
160
+ const fields = decl.fields
161
+ .map((field) => {
162
+ const tag = `json:"${field.name}${field.optional ? ",omitempty" : ""}"`;
163
+ return "\t" + goFieldName(field.name) + " " + goRef(field.ref) + " `" + tag + "`";
164
+ })
165
+ .join("\n");
166
+ return `type ${decl.name} struct {\n${fields}\n}`;
167
+ }
168
+
169
+ function goAliasOrDecls(
170
+ root: TypeRef,
171
+ decls: TypeDecl[],
172
+ rootName: string,
173
+ ): string {
174
+ const pieces = decls.map(goDecl);
175
+ if (root.t !== "named") pieces.unshift(`type ${rootName} = ${goRef(root)}`);
176
+ return pieces.join("\n\n");
177
+ }
178
+
179
+ function goStarter(ctx: LanguageContext): string {
180
+ const input = buildTypes(INPUT_ROOT, ctx.input);
181
+ const output = buildTypes(OUTPUT_ROOT, ctx.output);
182
+ const types = [
183
+ goAliasOrDecls(input.root, input.decls, INPUT_ROOT),
184
+ goAliasOrDecls(output.root, output.decls, OUTPUT_ROOT),
185
+ ].join("\n\n");
186
+ return `package main
187
+
188
+ import (
189
+ "encoding/json"
190
+ "fmt"
191
+ "io"
192
+ "os"
193
+ )
194
+
195
+ ${types}
196
+
197
+ func ${ctx.functionName}(payload ${INPUT_ROOT}) ${OUTPUT_ROOT} {
198
+ panic("Not implemented: ${ctx.functionName}")
199
+ }
200
+
201
+ func main() {
202
+ raw, _ := io.ReadAll(os.Stdin)
203
+ var payload ${INPUT_ROOT}
204
+ if len(raw) > 0 {
205
+ if err := json.Unmarshal(raw, &payload); err != nil {
206
+ panic(err)
207
+ }
208
+ }
209
+ out, err := json.Marshal(${ctx.functionName}(payload))
210
+ if err != nil {
211
+ panic(err)
212
+ }
213
+ fmt.Print(string(out))
214
+ }
215
+ `;
216
+ }
217
+
218
+ /* ------------------------------------------------------------------ */
219
+ /* Rust: serde structs */
220
+ /* ------------------------------------------------------------------ */
221
+
222
+ function rustRef(ref: TypeRef): string {
223
+ switch (ref.t) {
224
+ case "string":
225
+ return "String";
226
+ case "int":
227
+ return "i64";
228
+ case "double":
229
+ return "f64";
230
+ case "bool":
231
+ return "bool";
232
+ case "any":
233
+ case "null":
234
+ return "serde_json::Value";
235
+ case "array":
236
+ return `Vec<${rustRef(ref.of)}>`;
237
+ case "named":
238
+ return ref.name;
239
+ }
240
+ }
241
+
242
+ function rustField(name: string): string {
243
+ let value = snake(name);
244
+ if (isReserved(value)) value = `${value}_`;
245
+ return value;
246
+ }
247
+
248
+ function rustDecl(decl: TypeDecl): string {
249
+ const fields = decl.fields
250
+ .map((field) => {
251
+ const ident = rustField(field.name);
252
+ const base = rustRef(field.ref);
253
+ const ty = field.optional ? `Option<${base}>` : base;
254
+ const attrs: string[] = [];
255
+ if (ident !== field.name) attrs.push(` #[serde(rename = "${field.name}")]`);
256
+ if (field.optional) {
257
+ attrs.push(' #[serde(default, skip_serializing_if = "Option::is_none")]');
258
+ }
259
+ const attrText = attrs.length > 0 ? attrs.join("\n") + "\n" : "";
260
+ return `${attrText} ${ident}: ${ty},`;
261
+ })
262
+ .join("\n");
263
+ return `#[derive(Serialize, Deserialize)]\nstruct ${decl.name} {\n${fields}\n}`;
264
+ }
265
+
266
+ function rustAliasOrDecls(root: TypeRef, decls: TypeDecl[], rootName: string): string {
267
+ const pieces = decls.map(rustDecl);
268
+ if (root.t !== "named") pieces.unshift(`type ${rootName} = ${rustRef(root)};`);
269
+ return pieces.join("\n\n");
270
+ }
271
+
272
+ function rustStarter(ctx: LanguageContext): string {
273
+ const input = buildTypes(INPUT_ROOT, ctx.input);
274
+ const output = buildTypes(OUTPUT_ROOT, ctx.output);
275
+ const types = [
276
+ rustAliasOrDecls(input.root, input.decls, INPUT_ROOT),
277
+ rustAliasOrDecls(output.root, output.decls, OUTPUT_ROOT),
278
+ ].join("\n\n");
279
+ return `use serde::{Deserialize, Serialize};
280
+ use std::io::Read;
281
+
282
+ ${types}
283
+
284
+ fn ${ctx.functionName}(payload: ${INPUT_ROOT}) -> ${OUTPUT_ROOT} {
285
+ let _ = &payload;
286
+ panic!("Not implemented: ${ctx.functionName}");
287
+ }
288
+
289
+ fn main() {
290
+ let mut raw = String::new();
291
+ std::io::stdin().read_to_string(&mut raw).expect("read stdin");
292
+ let trimmed = raw.trim();
293
+ let source = if trimmed.is_empty() { "null" } else { trimmed };
294
+ let payload: ${INPUT_ROOT} = serde_json::from_str(source).expect("parse json");
295
+ let result = ${ctx.functionName}(payload);
296
+ println!("{}", serde_json::to_string(&result).expect("serialize json"));
297
+ }
298
+ `;
299
+ }
300
+
301
+ /* ------------------------------------------------------------------ */
302
+ /* C#: classes with JsonPropertyName */
303
+ /* ------------------------------------------------------------------ */
304
+
305
+ function csRef(ref: TypeRef): string {
306
+ switch (ref.t) {
307
+ case "string":
308
+ return "string";
309
+ case "int":
310
+ return "long";
311
+ case "double":
312
+ return "double";
313
+ case "bool":
314
+ return "bool";
315
+ case "any":
316
+ case "null":
317
+ return "JsonNode";
318
+ case "array":
319
+ return `List<${csRef(ref.of)}>`;
320
+ case "named":
321
+ return ref.name;
322
+ }
323
+ }
324
+
325
+ function csFieldName(name: string): string {
326
+ const value = pascal(name);
327
+ return value.length > 0 ? value : "Field";
328
+ }
329
+
330
+ function csDecl(decl: TypeDecl): string {
331
+ const props = decl.fields
332
+ .map((field) => {
333
+ return ` [JsonPropertyName("${field.name}")]\n public ${csRef(field.ref)} ${csFieldName(
334
+ field.name,
335
+ )} { get; set; }`;
336
+ })
337
+ .join("\n\n");
338
+ return `class ${decl.name}\n{\n${props}\n}`;
339
+ }
340
+
341
+ function csStarter(ctx: LanguageContext): string {
342
+ const input = buildTypes(INPUT_ROOT, ctx.input);
343
+ const output = buildTypes(OUTPUT_ROOT, ctx.output);
344
+ const decls = [...input.decls, ...output.decls].map(csDecl).join("\n\n");
345
+ const inputType = csRef(input.root);
346
+ const outputType = csRef(output.root);
347
+ return `using System;
348
+ using System.Collections.Generic;
349
+ using System.Text.Json;
350
+ using System.Text.Json.Nodes;
351
+ using System.Text.Json.Serialization;
352
+
353
+ ${decls}
354
+
355
+ class Program
356
+ {
357
+ static ${outputType} ${ctx.functionName}(${inputType} payload)
358
+ {
359
+ throw new NotImplementedException("Not implemented: ${ctx.functionName}");
360
+ }
361
+
362
+ static void Main()
363
+ {
364
+ var raw = Console.In.ReadToEnd().Trim();
365
+ var payload = JsonSerializer.Deserialize<${inputType}>(raw);
366
+ Console.Write(JsonSerializer.Serialize(${ctx.functionName}(payload)));
367
+ }
368
+ }
369
+ `;
370
+ }
371
+
372
+ /* ------------------------------------------------------------------ */
373
+ /* Swift: Codable structs (only when the schema is fully typed) */
374
+ /* ------------------------------------------------------------------ */
375
+
376
+ function swiftRef(ref: TypeRef): string {
377
+ switch (ref.t) {
378
+ case "string":
379
+ return "String";
380
+ case "int":
381
+ return "Int";
382
+ case "double":
383
+ return "Double";
384
+ case "bool":
385
+ return "Bool";
386
+ case "array":
387
+ return `[${swiftRef(ref.of)}]`;
388
+ case "named":
389
+ return ref.name;
390
+ default:
391
+ return "Any";
392
+ }
393
+ }
394
+
395
+ function swiftFieldName(name: string): string {
396
+ if (isIdentifier(name) && !isReserved(name)) return name;
397
+ let value = snake(name);
398
+ if (isReserved(value)) value = `${value}_`;
399
+ return isIdentifier(value) ? value : "field";
400
+ }
401
+
402
+ function swiftDecl(decl: TypeDecl): string {
403
+ const fields: string[] = [];
404
+ const codingKeys: string[] = [];
405
+ let needsKeys = false;
406
+ for (const field of decl.fields) {
407
+ const prop = swiftFieldName(field.name);
408
+ const base = swiftRef(field.ref);
409
+ const ty = field.optional ? `${base}?` : base;
410
+ fields.push(` let ${prop}: ${ty}`);
411
+ if (prop !== field.name) {
412
+ needsKeys = true;
413
+ codingKeys.push(` case ${prop} = "${field.name}"`);
414
+ } else {
415
+ codingKeys.push(` case ${prop}`);
416
+ }
417
+ }
418
+ const keys = needsKeys ? `\n\n enum CodingKeys: String, CodingKey {\n${codingKeys.join("\n")}\n }` : "";
419
+ return `struct ${decl.name}: Codable {\n${fields.join("\n")}${keys}\n}`;
420
+ }
421
+
422
+ function swiftAliasOrDecls(root: TypeRef, decls: TypeDecl[], rootName: string): string {
423
+ const pieces = decls.map(swiftDecl);
424
+ if (root.t !== "named") pieces.unshift(`typealias ${rootName} = ${swiftRef(root)}`);
425
+ return pieces.join("\n\n");
426
+ }
427
+
428
+ function swiftStarter(ctx: LanguageContext): string {
429
+ const input = buildTypes(INPUT_ROOT, ctx.input);
430
+ const output = buildTypes(OUTPUT_ROOT, ctx.output);
431
+ const types = [
432
+ swiftAliasOrDecls(input.root, input.decls, INPUT_ROOT),
433
+ swiftAliasOrDecls(output.root, output.decls, OUTPUT_ROOT),
434
+ ].join("\n\n");
435
+ return `import Foundation
436
+
437
+ ${types}
438
+
439
+ func ${ctx.functionName}(_ payload: ${INPUT_ROOT}) -> ${OUTPUT_ROOT} {
440
+ fatalError("Not implemented: ${ctx.functionName}")
441
+ }
442
+
443
+ let raw = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""
444
+ let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
445
+ let data = trimmed.isEmpty ? Data("null".utf8) : Data(raw.utf8)
446
+ let payload = try! JSONDecoder().decode(${INPUT_ROOT}.self, from: data)
447
+ let out = try! JSONEncoder().encode(${ctx.functionName}(payload))
448
+ FileHandle.standardOutput.write(out)
449
+ `;
450
+ }
451
+
452
+ function swiftFallbackStarter(ctx: LanguageContext): string {
453
+ return `import Foundation
454
+
455
+ ${formatComment("//", ctx)}
456
+
457
+ func ${ctx.functionName}(_ payload: Any) -> Any {
458
+ fatalError("Not implemented: ${ctx.functionName}")
459
+ }
460
+
461
+ let raw = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""
462
+ let data = raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? Data("null".utf8) : Data(raw.utf8)
463
+ let payload = try! JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
464
+ let out = try! JSONSerialization.data(withJSONObject: ${ctx.functionName}(payload), options: [.fragmentsAllowed])
465
+ FileHandle.standardOutput.write(out)
466
+ `;
467
+ }
468
+
43
469
  const adapters: Record<LanguageId, LanguageAdapter> = {
44
470
  typescript: {
45
471
  id: "typescript",
@@ -47,16 +473,8 @@ const adapters: Record<LanguageId, LanguageAdapter> = {
47
473
  aliases: ["ts"],
48
474
  extension: "ts",
49
475
  runtime: "Node.js 22.6+ (uses `--experimental-strip-types`)",
50
- starter: ({ functionName }) => `import { readFileSync } from "node:fs";
51
-
52
- function ${functionName}(payload: unknown): unknown {
53
- throw new Error("Not implemented: ${functionName}");
54
- }
55
-
56
- const input = readFileSync(0, "utf8");
57
- const payload = JSON.parse(input.trim() === "" ? "null" : input);
58
- process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
59
- `,
476
+ typed: true,
477
+ starter: typescriptStarter,
60
478
  files: (ctx) => [
61
479
  packageJson(ctx.projectName, "node tests/runner.mjs"),
62
480
  {
@@ -81,7 +499,7 @@ process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
81
499
  },
82
500
  ],
83
501
  build: () => null,
84
- run: ({ functionName }) => `node --experimental-strip-types src/solution.ts`,
502
+ run: () => "node --experimental-strip-types src/solution.ts",
85
503
  },
86
504
 
87
505
  javascript: {
@@ -90,15 +508,18 @@ process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
90
508
  aliases: ["js", "node"],
91
509
  extension: "mjs",
92
510
  runtime: "Node.js 18+",
93
- starter: ({ functionName }) => `function ${functionName}(payload) {
94
- throw new Error("Not implemented: ${functionName}");
511
+ typed: false,
512
+ starter: (ctx) => `${formatComment("//", ctx)}
513
+
514
+ function ${ctx.functionName}(payload) {
515
+ throw new Error("Not implemented: ${ctx.functionName}");
95
516
  }
96
517
 
97
518
  const chunks = [];
98
519
  for await (const chunk of process.stdin) chunks.push(chunk);
99
520
  const input = Buffer.concat(chunks).toString("utf8");
100
521
  const payload = JSON.parse(input.trim() === "" ? "null" : input);
101
- process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
522
+ process.stdout.write(JSON.stringify(${ctx.functionName}(payload) ?? null));
102
523
  `,
103
524
  files: (ctx) => [packageJson(ctx.projectName, "node tests/runner.mjs")],
104
525
  build: () => null,
@@ -111,18 +532,21 @@ process.stdout.write(JSON.stringify(${functionName}(payload) ?? null));
111
532
  aliases: ["py", "python3"],
112
533
  extension: "py",
113
534
  runtime: "Python 3.8+",
114
- starter: ({ functionName }) => `import json
535
+ typed: false,
536
+ starter: (ctx) => `${formatComment("#", ctx)}
537
+
538
+ import json
115
539
  import sys
116
540
 
117
541
 
118
- def ${functionName}(payload):
119
- raise NotImplementedError("Not implemented: ${functionName}")
542
+ def ${ctx.functionName}(payload):
543
+ raise NotImplementedError("Not implemented: ${ctx.functionName}")
120
544
 
121
545
 
122
546
  def main():
123
547
  raw = sys.stdin.read().strip()
124
548
  payload = json.loads(raw) if raw else None
125
- print(json.dumps(${functionName}(payload)))
549
+ print(json.dumps(${ctx.functionName}(payload)))
126
550
 
127
551
 
128
552
  if __name__ == "__main__":
@@ -139,15 +563,18 @@ if __name__ == "__main__":
139
563
  aliases: ["rb"],
140
564
  extension: "rb",
141
565
  runtime: "Ruby 2.6+ (json is in the standard library)",
142
- starter: ({ functionName }) => `require "json"
566
+ typed: false,
567
+ starter: (ctx) => `${formatComment("#", ctx)}
568
+
569
+ require "json"
143
570
 
144
- def ${functionName}(payload)
145
- raise NotImplementedError, "Not implemented: ${functionName}"
571
+ def ${ctx.functionName}(payload)
572
+ raise NotImplementedError, "Not implemented: ${ctx.functionName}"
146
573
  end
147
574
 
148
575
  raw = $stdin.read.strip
149
576
  payload = raw.empty? ? nil : JSON.parse(raw)
150
- puts JSON.generate(${functionName}(payload))
577
+ puts JSON.generate(${ctx.functionName}(payload))
151
578
  `,
152
579
  files: () => [],
153
580
  build: () => null,
@@ -160,15 +587,17 @@ puts JSON.generate(${functionName}(payload))
160
587
  aliases: ["php8"],
161
588
  extension: "php",
162
589
  runtime: "PHP 7.4+",
163
- starter: ({ functionName }) => `<?php
590
+ typed: false,
591
+ starter: (ctx) => `<?php
592
+ ${formatComment("//", ctx)}
164
593
 
165
- function ${functionName}($payload) {
166
- throw new Exception("Not implemented: ${functionName}");
594
+ function ${ctx.functionName}($payload) {
595
+ throw new Exception("Not implemented: ${ctx.functionName}");
167
596
  }
168
597
 
169
598
  $raw = trim(stream_get_contents(STDIN));
170
599
  $payload = $raw === "" ? null : json_decode($raw, true);
171
- echo json_encode(${functionName}($payload));
600
+ echo json_encode(${ctx.functionName}($payload));
172
601
  `,
173
602
  files: () => [],
174
603
  build: () => null,
@@ -180,38 +609,10 @@ echo json_encode(${functionName}($payload));
180
609
  name: "Go",
181
610
  aliases: ["golang"],
182
611
  extension: "go",
183
- runtime: "Go 1.18+ (uses `any`)",
184
- starter: ({ functionName }) => `package main
185
-
186
- import (
187
- "encoding/json"
188
- "fmt"
189
- "io"
190
- "os"
191
- )
192
-
193
- func ${functionName}(payload any) any {
194
- panic("Not implemented: ${functionName}")
195
- }
196
-
197
- func main() {
198
- raw, _ := io.ReadAll(os.Stdin)
199
- var payload any
200
- if len(raw) > 0 {
201
- if err := json.Unmarshal(raw, &payload); err != nil {
202
- panic(err)
203
- }
204
- }
205
- out, err := json.Marshal(${functionName}(payload))
206
- if err != nil {
207
- panic(err)
208
- }
209
- fmt.Print(string(out))
210
- }
211
- `,
212
- files: () => [
213
- { relative: "go.mod", contents: "module practice\n\ngo 1.21\n" },
214
- ],
612
+ runtime: "Go 1.18+ (`any`)",
613
+ typed: true,
614
+ starter: goStarter,
615
+ files: () => [{ relative: "go.mod", contents: "module practice\n\ngo 1.21\n" }],
215
616
  build: () => "mkdir -p out && go build -o out/solution src/solution.go",
216
617
  run: () => "./out/solution",
217
618
  },
@@ -221,28 +622,9 @@ func main() {
221
622
  name: "Rust",
222
623
  aliases: ["rs", "cargo"],
223
624
  extension: "rs",
224
- runtime: "Rust 1.70+ (uses serde_json)",
225
- starter: ({ functionName }) => `use serde_json::Value;
226
- use std::io::Read;
227
-
228
- fn ${functionName}(payload: Value) -> Value {
229
- let _ = payload;
230
- panic!("Not implemented: ${functionName}");
231
- }
232
-
233
- fn main() {
234
- let mut raw = String::new();
235
- std::io::stdin().read_to_string(&mut raw).expect("read stdin");
236
- let trimmed = raw.trim();
237
- let payload: Value = if trimmed.is_empty() {
238
- Value::Null
239
- } else {
240
- serde_json::from_str(trimmed).expect("parse json")
241
- };
242
- let result = ${functionName}(payload);
243
- println!("{}", serde_json::to_string(&result).expect("serialize json"));
244
- }
245
- `,
625
+ runtime: "Rust 1.70+ (serde + serde_json)",
626
+ typed: true,
627
+ starter: rustStarter,
246
628
  files: (ctx) => [
247
629
  {
248
630
  relative: "Cargo.toml",
@@ -252,6 +634,7 @@ version = "0.1.0"
252
634
  edition = "2021"
253
635
 
254
636
  [dependencies]
637
+ serde = { version = "1", features = ["derive"] }
255
638
  serde_json = "1"
256
639
 
257
640
  [[bin]]
@@ -270,24 +653,8 @@ path = "src/solution.rs"
270
653
  aliases: ["cs", "dotnet"],
271
654
  extension: "cs",
272
655
  runtime: ".NET 8 SDK",
273
- starter: ({ functionName }) => `using System;
274
- using System.Text.Json.Nodes;
275
-
276
- class Program
277
- {
278
- static JsonNode? ${functionName}(JsonNode? payload)
279
- {
280
- throw new NotImplementedException("Not implemented: ${functionName}");
281
- }
282
-
283
- static void Main()
284
- {
285
- var raw = Console.In.ReadToEnd().Trim();
286
- var payload = raw.Length == 0 ? null : JsonNode.Parse(raw);
287
- Console.Write(${functionName}(payload)?.ToJsonString() ?? "null");
288
- }
289
- }
290
- `,
656
+ typed: true,
657
+ starter: csStarter,
291
658
  files: (ctx) => [
292
659
  {
293
660
  relative: `${ctx.projectName}.csproj`,
@@ -296,7 +663,7 @@ class Program
296
663
  <OutputType>Exe</OutputType>
297
664
  <TargetFramework>net8.0</TargetFramework>
298
665
  <ImplicitUsings>enable</ImplicitUsings>
299
- <Nullable>enable</Nullable>
666
+ <Nullable>disable</Nullable>
300
667
  <AssemblyName>solution</AssemblyName>
301
668
  </PropertyGroup>
302
669
  </Project>
@@ -312,22 +679,9 @@ class Program
312
679
  name: "Swift",
313
680
  aliases: [],
314
681
  extension: "swift",
315
- runtime: "Swift 5.5+ (Foundation JSONSerialization)",
316
- starter: ({ functionName }) => `import Foundation
317
-
318
- func ${functionName}(_ payload: Any) -> Any {
319
- fatalError("Not implemented: ${functionName}")
320
- }
321
-
322
- let raw = String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8) ?? ""
323
- let data = raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
324
- ? Data("null".utf8)
325
- : Data(raw.utf8)
326
- let payload = try! JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
327
- let result = ${functionName}(payload)
328
- let out = try! JSONSerialization.data(withJSONObject: result, options: [.fragmentsAllowed])
329
- FileHandle.standardOutput.write(out)
330
- `,
682
+ runtime: "Swift 5.5+",
683
+ typed: true,
684
+ starter: (ctx) => (isFullyTyped(ctx.input) && isFullyTyped(ctx.output) ? swiftStarter(ctx) : swiftFallbackStarter(ctx)),
331
685
  files: () => [],
332
686
  build: () => "mkdir -p out && swiftc -O src/solution.swift -o out/solution",
333
687
  run: () => "./out/solution",
package/src/prompt.ts CHANGED
@@ -88,7 +88,12 @@ Call the tool \`leetcode_scaffold\` with a spec containing:
88
88
  "javascript", "python", "ruby", "php", "go", "rust", "csharp", "swift")
89
89
  - requirements: concrete, testable bullet points
90
90
  - edgeCases: tricky situations the solution must handle
91
- - starterCode: optional; otherwise a TODO stub is generated
91
+ - Do NOT provide starterCode. Omit it so the plugin can generate a starter that
92
+ reflects the real types: type-safe languages get typed interfaces/structs and a
93
+ typed signature; dynamic languages get a documented input/output format comment.
94
+ The generated types are inferred from your publicTests/hiddenTests, so make the
95
+ first few cases representative of the full input shape (and vary fields so
96
+ optional ones are detected).
92
97
  - publicTests: 5-8 visible cases that illustrate the contract
93
98
  - hiddenTests: 6-12 acceptance cases, including edge cases, boundary values, an
94
99
  empty/degenerate input, and at least one larger input
package/src/scaffold.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { getLanguage, normalizeLanguage } from "./languages";
3
+ import { getLanguage, normalizeLanguage, type LanguageContext } from "./languages";
4
4
  import { assertNoLeak } from "./patterns";
5
+ import { schemaFromSamples } from "./schema";
5
6
  import type { FileEntry, LanguageId, ScaffoldResult, ScaffoldSpec, TestCase } from "./types";
6
7
 
7
8
  function kebab(input: string): string {
@@ -251,7 +252,17 @@ function casesFile(cases: TestCase[]): string {
251
252
 
252
253
  function buildFiles(spec: ScaffoldSpec, names: ResolvedNames, language: LanguageId): FileEntry[] {
253
254
  const adapter = getLanguage(language);
254
- const ctx = { projectName: names.projectName, functionName: names.functionName, title: names.title };
255
+ const cases = [...(spec.publicTests ?? []), ...(spec.hiddenTests ?? [])];
256
+ const first = cases[0];
257
+ const ctx: LanguageContext = {
258
+ projectName: names.projectName,
259
+ functionName: names.functionName,
260
+ title: names.title,
261
+ input: schemaFromSamples(cases.map((test) => test.input)),
262
+ output: schemaFromSamples(cases.map((test) => test.expected)),
263
+ inputExample: first?.input ?? null,
264
+ outputExample: first?.expected ?? null,
265
+ };
255
266
 
256
267
  const files: FileEntry[] = [
257
268
  { relative: "README.md", contents: readmeFor(names, spec, language) },
package/src/schema.ts ADDED
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Infers a small structural schema from the sample test cases so the generated
3
+ * starter files can carry real types (type-safe languages) or a documented
4
+ * format (dynamic languages).
5
+ *
6
+ * This is intentionally schema-light: it only needs enough fidelity to describe
7
+ * JSON input/output shapes (objects, arrays, primitives, optionality).
8
+ */
9
+
10
+ export type SchemaNode =
11
+ | { kind: "string" }
12
+ | { kind: "number"; integer: boolean }
13
+ | { kind: "boolean" }
14
+ | { kind: "null" }
15
+ | { kind: "unknown" }
16
+ | { kind: "array"; element: SchemaNode }
17
+ | { kind: "object"; fields: FieldSchema[] };
18
+
19
+ export interface FieldSchema {
20
+ name: string;
21
+ schema: SchemaNode;
22
+ optional: boolean;
23
+ }
24
+
25
+ export function inferSchema(value: unknown): SchemaNode {
26
+ if (value === null || value === undefined) return { kind: "null" };
27
+ if (Array.isArray(value)) {
28
+ return { kind: "array", element: mergeMany(value.map(inferSchema)) };
29
+ }
30
+ switch (typeof value) {
31
+ case "string":
32
+ return { kind: "string" };
33
+ case "boolean":
34
+ return { kind: "boolean" };
35
+ case "number":
36
+ return { kind: "number", integer: Number.isInteger(value) };
37
+ case "object": {
38
+ const fields = Object.entries(value as Record<string, unknown>).map(([name, entry]) => ({
39
+ name,
40
+ schema: inferSchema(entry),
41
+ optional: false,
42
+ }));
43
+ return { kind: "object", fields };
44
+ }
45
+ default:
46
+ return { kind: "unknown" };
47
+ }
48
+ }
49
+
50
+ export function mergeSchemas(a: SchemaNode, b: SchemaNode): SchemaNode {
51
+ if (a.kind === "unknown") return b;
52
+ if (b.kind === "unknown") return a;
53
+ if (a.kind === "null") return b;
54
+ if (b.kind === "null") return a;
55
+
56
+ if (a.kind === "object" && b.kind === "object") {
57
+ const names = new Set([...a.fields.map((f) => f.name), ...b.fields.map((f) => f.name)]);
58
+ const fields: FieldSchema[] = [];
59
+ for (const name of names) {
60
+ const left = a.fields.find((f) => f.name === name);
61
+ const right = b.fields.find((f) => f.name === name);
62
+ if (left && right) {
63
+ fields.push({
64
+ name,
65
+ schema: mergeSchemas(left.schema, right.schema),
66
+ optional: left.optional && right.optional,
67
+ });
68
+ } else {
69
+ const only = (left ?? right)!;
70
+ fields.push({ name, schema: only.schema, optional: true });
71
+ }
72
+ }
73
+ return { kind: "object", fields };
74
+ }
75
+
76
+ if (a.kind === "array" && b.kind === "array") {
77
+ return { kind: "array", element: mergeSchemas(a.element, b.element) };
78
+ }
79
+
80
+ if (a.kind === "number" && b.kind === "number") {
81
+ return { kind: "number", integer: a.integer && b.integer };
82
+ }
83
+ if (a.kind === b.kind) return a;
84
+ return { kind: "unknown" };
85
+ }
86
+
87
+ function mergeMany(nodes: SchemaNode[]): SchemaNode {
88
+ if (nodes.length === 0) return { kind: "unknown" };
89
+ return nodes.reduce((acc, node) => mergeSchemas(acc, node));
90
+ }
91
+
92
+ export function schemaFromSamples(values: unknown[]): SchemaNode {
93
+ return mergeMany(values.map(inferSchema));
94
+ }
95
+
96
+ /** True when the schema contains only types representable as static types. */
97
+ export function isFullyTyped(node: SchemaNode): boolean {
98
+ switch (node.kind) {
99
+ case "unknown":
100
+ case "null":
101
+ return false;
102
+ case "array":
103
+ return isFullyTyped(node.element);
104
+ case "object":
105
+ return node.fields.every((field) => isFullyTyped(field.schema));
106
+ default:
107
+ return true;
108
+ }
109
+ }
110
+
111
+ /** Human-readable signature, e.g. `{ amounts: number[]; target: number }`. */
112
+ export function schemaSignature(node: SchemaNode): string {
113
+ switch (node.kind) {
114
+ case "string":
115
+ return "string";
116
+ case "number":
117
+ return "number";
118
+ case "boolean":
119
+ return "boolean";
120
+ case "null":
121
+ return "null";
122
+ case "unknown":
123
+ return "any";
124
+ case "array": {
125
+ const inner = schemaSignature(node.element);
126
+ return inner.includes(" ") ? `(${inner})[]` : `${inner}[]`;
127
+ }
128
+ case "object":
129
+ if (node.fields.length === 0) return "{}";
130
+ return `{ ${node.fields
131
+ .map((field) => `${field.name}${field.optional ? "?" : ""}: ${schemaSignature(field.schema)}`)
132
+ .join("; ")} }`;
133
+ }
134
+ }
135
+
136
+ export type TypeRef =
137
+ | { t: "string" }
138
+ | { t: "int" }
139
+ | { t: "double" }
140
+ | { t: "bool" }
141
+ | { t: "any" }
142
+ | { t: "null" }
143
+ | { t: "array"; of: TypeRef }
144
+ | { t: "named"; name: string };
145
+
146
+ export interface TypeDecl {
147
+ name: string;
148
+ fields: { name: string; ref: TypeRef; optional: boolean }[];
149
+ }
150
+
151
+ export function buildTypes(
152
+ rootName: string,
153
+ node: SchemaNode,
154
+ ): { root: TypeRef; decls: TypeDecl[] } {
155
+ const decls: TypeDecl[] = [];
156
+ const root = refFor(rootName, node, decls);
157
+ return { root, decls };
158
+ }
159
+
160
+ function refFor(name: string, node: SchemaNode, decls: TypeDecl[]): TypeRef {
161
+ switch (node.kind) {
162
+ case "object": {
163
+ const decl: TypeDecl = { name, fields: [] };
164
+ decls.push(decl);
165
+ for (const field of node.fields) {
166
+ decl.fields.push({
167
+ name: field.name,
168
+ optional: field.optional,
169
+ ref: refFor(`${name}${pascal(field.name)}`, field.schema, decls),
170
+ });
171
+ }
172
+ return { t: "named", name };
173
+ }
174
+ case "array":
175
+ return { t: "array", of: refFor(`${name}Item`, node.element, decls) };
176
+ case "string":
177
+ return { t: "string" };
178
+ case "boolean":
179
+ return { t: "bool" };
180
+ case "null":
181
+ return { t: "null" };
182
+ case "unknown":
183
+ return { t: "any" };
184
+ case "number":
185
+ return { t: node.integer ? "int" : "double" };
186
+ }
187
+ }
188
+
189
+ export function pascal(input: string): string {
190
+ const parts = input
191
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
192
+ .split(/[^a-zA-Z0-9]+/)
193
+ .filter(Boolean);
194
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
195
+ }
196
+
197
+ export function snake(input: string): string {
198
+ const value = input
199
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
200
+ .replace(/[^a-zA-Z0-9]+/g, "_")
201
+ .replace(/^_+|_+$/g, "")
202
+ .toLowerCase();
203
+ return /^[0-9]/.test(value) ? `_${value}` : value || "field";
204
+ }
205
+
206
+ const RESERVED = new Set([
207
+ "abstract", "as", "async", "await", "base", "bool", "break", "byte", "case", "catch",
208
+ "class", "const", "continue", "crate", "default", "delegate", "do", "double", "dyn",
209
+ "else", "enum", "event", "explicit", "extern", "false", "final", "finally", "fixed",
210
+ "float", "fn", "for", "foreach", "func", "goto", "if", "impl", "implements", "import",
211
+ "in", "int", "interface", "internal", "is", "let", "lock", "long", "loop", "match",
212
+ "mod", "move", "mut", "namespace", "new", "null", "object", "operator", "out",
213
+ "override", "package", "params", "private", "protected", "pub", "public", "readonly",
214
+ "ref", "return", "self", "short", "sizeof", "static", "string", "struct", "super",
215
+ "switch", "this", "throw", "trait", "true", "try", "type", "typeof", "uint", "ulong",
216
+ "unchecked", "unsafe", "use", "ushort", "var", "virtual", "void", "volatile", "where",
217
+ "while", "with", "yield",
218
+ ]);
219
+
220
+ export function isReserved(word: string): boolean {
221
+ return RESERVED.has(word.toLowerCase());
222
+ }
223
+
224
+ export function isIdentifier(word: string): boolean {
225
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(word);
226
+ }