ic-mops 2.1.0 → 2.2.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 (40) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/RELEASE.md +19 -0
  3. package/bundle/cli.tgz +0 -0
  4. package/cli.ts +15 -0
  5. package/commands/bench.ts +11 -3
  6. package/commands/build.ts +3 -2
  7. package/commands/check.ts +4 -0
  8. package/commands/test/test.ts +3 -1
  9. package/commands/watch/error-checker.ts +8 -2
  10. package/commands/watch/warning-checker.ts +8 -2
  11. package/dist/cli.js +13 -1
  12. package/dist/commands/bench.js +5 -4
  13. package/dist/commands/build.js +3 -2
  14. package/dist/commands/check.js +4 -0
  15. package/dist/commands/test/test.js +3 -1
  16. package/dist/commands/watch/error-checker.js +8 -2
  17. package/dist/commands/watch/warning-checker.js +8 -2
  18. package/dist/error.d.ts +1 -1
  19. package/dist/helpers/autofix-motoko.js +84 -39
  20. package/dist/mops.d.ts +1 -0
  21. package/dist/mops.js +10 -0
  22. package/dist/package.json +1 -1
  23. package/dist/tests/check-fix.test.js +16 -0
  24. package/dist/tests/check.test.js +4 -0
  25. package/dist/tests/moc-args.test.d.ts +1 -0
  26. package/dist/tests/moc-args.test.js +17 -0
  27. package/dist/types.d.ts +3 -0
  28. package/error.ts +1 -1
  29. package/helpers/autofix-motoko.ts +119 -49
  30. package/mops.ts +13 -0
  31. package/package.json +1 -1
  32. package/tests/__snapshots__/check-fix.test.ts.snap +25 -6
  33. package/tests/__snapshots__/check.test.ts.snap +9 -0
  34. package/tests/check/fix/overlapping.mo +10 -0
  35. package/tests/check/moc-args/Warning.mo +5 -0
  36. package/tests/check/moc-args/mops.toml +2 -0
  37. package/tests/check-fix.test.ts +23 -0
  38. package/tests/check.test.ts +5 -0
  39. package/tests/moc-args.test.ts +19 -0
  40. package/types.ts +3 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.2.0
6
+ - Add `[moc]` config section for global `moc` compiler flags (applied to `check`, `build`, `test`, `bench`, `watch`)
7
+ - Add `mops moc-args` command to print global `moc` flags from `[moc]` config section
8
+ - Fix `mops check --fix` crash on overlapping diagnostic edits (e.g., nested function calls)
9
+
5
10
  ## 2.1.0
6
11
  - Add `mops check --fix` subcommand (for Motoko files) with autofix logic
7
12
  - Add `mops check` subcommand for type-checking Motoko files
package/RELEASE.md CHANGED
@@ -130,6 +130,25 @@ dfx deploy --network ic --no-wallet cli --identity mops
130
130
 
131
131
  This deploys the `cli-releases` canister (serving `cli.mops.one`) to the Internet Computer mainnet.
132
132
 
133
+ ### 10. Commit and push release artifacts
134
+
135
+ Step 8 generates files in `cli-releases/` that must be committed and pushed:
136
+
137
+ ```bash
138
+ git add cli-releases/
139
+ git commit -m "cli-releases: v<version> artifacts"
140
+ ```
141
+
142
+ Since direct pushes to `main` are not allowed, create a branch and PR:
143
+
144
+ ```bash
145
+ git checkout -b <username>/release-X.Y.Z-artifacts
146
+ git push -u origin <username>/release-X.Y.Z-artifacts
147
+ gh pr create --title "cli-releases: vX.Y.Z artifacts" --body "Release artifacts generated by \`npm run release-cli\` for CLI vX.Y.Z."
148
+ ```
149
+
150
+ Merge this PR after approval.
151
+
133
152
  ## Verify build
134
153
 
135
154
  Anyone can verify a released version by rebuilding from source:
package/bundle/cli.tgz CHANGED
Binary file
package/cli.ts CHANGED
@@ -46,7 +46,9 @@ import {
46
46
  apiVersion,
47
47
  checkApiCompatibility,
48
48
  checkConfigFile,
49
+ getGlobalMocArgs,
49
50
  getNetworkFile,
51
+ readConfig,
50
52
  setNetwork,
51
53
  version,
52
54
  } from "./mops.js";
@@ -255,6 +257,19 @@ program
255
257
  console.log(sourcesArr.join("\n"));
256
258
  });
257
259
 
260
+ // moc-args
261
+ program
262
+ .command("moc-args")
263
+ .description("Print global moc compiler flags from [moc] config section")
264
+ .action(async () => {
265
+ checkConfigFile(true);
266
+ let config = readConfig();
267
+ let args = getGlobalMocArgs(config);
268
+ if (args.length) {
269
+ console.log(args.join("\n"));
270
+ }
271
+ });
272
+
258
273
  // search
259
274
  program
260
275
  .command("search <text>")
package/commands/bench.ts CHANGED
@@ -12,7 +12,12 @@ import { filesize } from "filesize";
12
12
  import terminalSize from "terminal-size";
13
13
  import { SemVer } from "semver";
14
14
 
15
- import { getRootDir, readConfig, readDfxJson } from "../mops.js";
15
+ import {
16
+ getGlobalMocArgs,
17
+ getRootDir,
18
+ readConfig,
19
+ readDfxJson,
20
+ } from "../mops.js";
16
21
  import { parallel } from "../parallel.js";
17
22
  import { absToRel } from "./test/utils.js";
18
23
  import { getMocVersion } from "../helpers/get-moc-version.js";
@@ -138,13 +143,15 @@ export async function bench(
138
143
 
139
144
  await replica.start({ silent: options.silent });
140
145
 
146
+ let globalMocArgs = getGlobalMocArgs(config);
147
+
141
148
  if (!process.env.CI && !options.silent) {
142
149
  console.log("Deploying canisters...");
143
150
  }
144
151
 
145
152
  await parallel(os.cpus().length, files, async (file: string) => {
146
153
  try {
147
- await deployBenchFile(file, options, replica);
154
+ await deployBenchFile(file, options, replica, globalMocArgs);
148
155
  } catch (err) {
149
156
  console.error("Unexpected error. Stopping replica...");
150
157
  await replica.stop();
@@ -267,6 +274,7 @@ async function deployBenchFile(
267
274
  file: string,
268
275
  options: BenchOptions,
269
276
  replica: BenchReplica,
277
+ globalMocArgs: string[],
270
278
  ): Promise<void> {
271
279
  let rootDir = getRootDir();
272
280
  let tempDir = path.join(rootDir, ".mops/.bench/", path.parse(file).name);
@@ -294,7 +302,7 @@ async function deployBenchFile(
294
302
  let mocArgs = getMocArgs(options);
295
303
  options.verbose && console.time(`build ${canisterName}`);
296
304
  await execaCommand(
297
- `${mocPath} -c --idl canister.mo ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`,
305
+ `${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`,
298
306
  {
299
307
  cwd: tempDir,
300
308
  stdio: options.verbose ? "pipe" : ["pipe", "ignore", "pipe"],
package/commands/build.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { cliError } from "../error.js";
7
7
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
8
8
  import { CustomSection, getWasmBindings } from "../wasm.js";
9
- import { readConfig } from "../mops.js";
9
+ import { getGlobalMocArgs, readConfig } from "../mops.js";
10
10
  import { CanisterConfig } from "../types.js";
11
11
  import { sourcesArgs } from "./sources.js";
12
12
  import { toolchain } from "./toolchain/index.js";
@@ -82,8 +82,8 @@ export async function build(
82
82
  "-o",
83
83
  wasmPath,
84
84
  motokoPath,
85
- ...(options.extraArgs ?? []),
86
85
  ...(await sourcesArgs()).flat(),
86
+ ...getGlobalMocArgs(config),
87
87
  ];
88
88
  if (config.build?.args) {
89
89
  if (typeof config.build.args === "string") {
@@ -101,6 +101,7 @@ export async function build(
101
101
  }
102
102
  args.push(...canister.args);
103
103
  }
104
+ args.push(...(options.extraArgs ?? []));
104
105
  const isPublicCandid = true; // always true for now to reduce corner cases
105
106
  const candidVisibility = isPublicCandid ? "icp:public" : "icp:private";
106
107
  if (isPublicCandid) {
package/commands/check.ts CHANGED
@@ -2,6 +2,7 @@ import { relative } from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { execa } from "execa";
4
4
  import { cliError } from "../error.js";
5
+ import { getGlobalMocArgs, readConfig } from "../mops.js";
5
6
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
6
7
  import { getMocSemVer } from "../helpers/get-moc-version.js";
7
8
  import { sourcesArgs } from "./sources.js";
@@ -30,8 +31,10 @@ export async function check(
30
31
  cliError("No Motoko files specified for checking");
31
32
  }
32
33
 
34
+ const config = readConfig();
33
35
  const mocPath = await toolchain.bin("moc", { fallback: true });
34
36
  const sources = await sourcesArgs();
37
+ const globalMocArgs = getGlobalMocArgs(config);
35
38
 
36
39
  // --all-libs enables richer diagnostics with edit suggestions from moc (requires moc >= 1.3.0)
37
40
  const allLibs = supportsAllLibsFlag();
@@ -53,6 +56,7 @@ export async function check(
53
56
  "--check",
54
57
  ...(allLibs ? ["--all-libs"] : []),
55
58
  ...sources.flat(),
59
+ ...globalMocArgs,
56
60
  ...(options.extraArgs ?? []),
57
61
  ];
58
62
 
@@ -13,7 +13,7 @@ import { SemVer } from "semver";
13
13
  import { ActorMethod } from "@icp-sdk/core/agent";
14
14
 
15
15
  import { sources } from "../sources.js";
16
- import { getRootDir, readConfig } from "../../mops.js";
16
+ import { getGlobalMocArgs, getRootDir, readConfig } from "../../mops.js";
17
17
  import { parallel } from "../../parallel.js";
18
18
 
19
19
  import { MMF1 } from "./mmf1.js";
@@ -232,6 +232,7 @@ export async function testWithReporter(
232
232
 
233
233
  let config = readConfig();
234
234
  let sourcesArr = await sources();
235
+ let globalMocArgs = getGlobalMocArgs(config);
235
236
 
236
237
  if (!mocPath) {
237
238
  mocPath = await toolchain.bin("moc", { fallback: true });
@@ -298,6 +299,7 @@ export async function testWithReporter(
298
299
  "--hide-warnings",
299
300
  "--error-detail=2",
300
301
  ...sourcesArr.join(" ").split(" "),
302
+ ...globalMocArgs,
301
303
  file,
302
304
  ].filter((x) => x);
303
305
 
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import chalk from "chalk";
5
5
 
6
6
  import { getMocPath } from "../../helpers/get-moc-path.js";
7
- import { getRootDir } from "../../mops.js";
7
+ import { getGlobalMocArgs, getRootDir, readConfig } from "../../mops.js";
8
8
  import { sources } from "../sources.js";
9
9
  import { parallel } from "../../parallel.js";
10
10
  import { globMoFiles } from "./globMoFiles.js";
@@ -44,6 +44,7 @@ export class ErrorChecker {
44
44
  let rootDir = getRootDir();
45
45
  let mocPath = getMocPath();
46
46
  let deps = await sources({ cwd: rootDir });
47
+ let globalMocArgs = getGlobalMocArgs(readConfig());
47
48
 
48
49
  let paths = globMoFiles(rootDir);
49
50
 
@@ -54,7 +55,12 @@ export class ErrorChecker {
54
55
  try {
55
56
  await promisify(execFile)(
56
57
  mocPath,
57
- ["--check", ...deps.flatMap((x) => x.split(" ")), file],
58
+ [
59
+ "--check",
60
+ ...deps.flatMap((x) => x.split(" ")),
61
+ ...globalMocArgs,
62
+ file,
63
+ ],
58
64
  { cwd: rootDir },
59
65
  );
60
66
  } catch (error: any) {
@@ -4,7 +4,7 @@ import os from "node:os";
4
4
  import chalk from "chalk";
5
5
 
6
6
  import { getMocPath } from "../../helpers/get-moc-path.js";
7
- import { getRootDir } from "../../mops.js";
7
+ import { getGlobalMocArgs, getRootDir, readConfig } from "../../mops.js";
8
8
  import { sources } from "../sources.js";
9
9
  import { ErrorChecker } from "./error-checker.js";
10
10
  import { parallel } from "../../parallel.js";
@@ -70,6 +70,7 @@ export class WarningChecker {
70
70
  let rootDir = getRootDir();
71
71
  let mocPath = getMocPath();
72
72
  let deps = await sources({ cwd: rootDir });
73
+ let globalMocArgs = getGlobalMocArgs(readConfig());
73
74
  let paths = globMoFiles(rootDir);
74
75
 
75
76
  this.totalFiles = paths.length;
@@ -82,7 +83,12 @@ export class WarningChecker {
82
83
 
83
84
  let { stderr } = await promisify(execFile)(
84
85
  mocPath,
85
- ["--check", ...deps.flatMap((x) => x.split(" ")), file],
86
+ [
87
+ "--check",
88
+ ...deps.flatMap((x) => x.split(" ")),
89
+ ...globalMocArgs,
90
+ file,
91
+ ],
86
92
  { cwd: rootDir, signal },
87
93
  ).catch((error) => {
88
94
  if (error.code === "ABORT_ERR") {
package/dist/cli.js CHANGED
@@ -32,7 +32,7 @@ import { toolchain } from "./commands/toolchain/index.js";
32
32
  import { update } from "./commands/update.js";
33
33
  import { getPrincipal, getUserProp, importPem, setUserProp, } from "./commands/user.js";
34
34
  import { watch } from "./commands/watch/watch.js";
35
- import { apiVersion, checkApiCompatibility, checkConfigFile, getNetworkFile, setNetwork, version, } from "./mops.js";
35
+ import { apiVersion, checkApiCompatibility, checkConfigFile, getGlobalMocArgs, getNetworkFile, readConfig, setNetwork, version, } from "./mops.js";
36
36
  import { resolvePackages } from "./resolve-packages.js";
37
37
  import { TOOLCHAINS } from "./commands/toolchain/toolchain-utils.js";
38
38
  events.setMaxListeners(20);
@@ -193,6 +193,18 @@ program
193
193
  let sourcesArr = await sources(options);
194
194
  console.log(sourcesArr.join("\n"));
195
195
  });
196
+ // moc-args
197
+ program
198
+ .command("moc-args")
199
+ .description("Print global moc compiler flags from [moc] config section")
200
+ .action(async () => {
201
+ checkConfigFile(true);
202
+ let config = readConfig();
203
+ let args = getGlobalMocArgs(config);
204
+ if (args.length) {
205
+ console.log(args.join("\n"));
206
+ }
207
+ });
196
208
  // search
197
209
  program
198
210
  .command("search <text>")
@@ -11,7 +11,7 @@ import stringWidth from "string-width";
11
11
  import { filesize } from "filesize";
12
12
  import terminalSize from "terminal-size";
13
13
  import { SemVer } from "semver";
14
- import { getRootDir, readConfig, readDfxJson } from "../mops.js";
14
+ import { getGlobalMocArgs, getRootDir, readConfig, readDfxJson, } from "../mops.js";
15
15
  import { parallel } from "../parallel.js";
16
16
  import { absToRel } from "./test/utils.js";
17
17
  import { getMocVersion } from "../helpers/get-moc-version.js";
@@ -95,12 +95,13 @@ export async function bench(filter = "", optionsArg = {}) {
95
95
  }
96
96
  }
97
97
  await replica.start({ silent: options.silent });
98
+ let globalMocArgs = getGlobalMocArgs(config);
98
99
  if (!process.env.CI && !options.silent) {
99
100
  console.log("Deploying canisters...");
100
101
  }
101
102
  await parallel(os.cpus().length, files, async (file) => {
102
103
  try {
103
- await deployBenchFile(file, options, replica);
104
+ await deployBenchFile(file, options, replica, globalMocArgs);
104
105
  }
105
106
  catch (err) {
106
107
  console.error("Unexpected error. Stopping replica...");
@@ -191,7 +192,7 @@ function getMocArgs(options) {
191
192
  }
192
193
  return args;
193
194
  }
194
- async function deployBenchFile(file, options, replica) {
195
+ async function deployBenchFile(file, options, replica, globalMocArgs) {
195
196
  let rootDir = getRootDir();
196
197
  let tempDir = path.join(rootDir, ".mops/.bench/", path.parse(file).name);
197
198
  let canisterName = path.parse(file).name;
@@ -205,7 +206,7 @@ async function deployBenchFile(file, options, replica) {
205
206
  let mocPath = getMocPath();
206
207
  let mocArgs = getMocArgs(options);
207
208
  options.verbose && console.time(`build ${canisterName}`);
208
- await execaCommand(`${mocPath} -c --idl canister.mo ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`, {
209
+ await execaCommand(`${mocPath} -c --idl canister.mo ${globalMocArgs.join(" ")} ${mocArgs} ${(await sources({ cwd: tempDir })).join(" ")}`, {
209
210
  cwd: tempDir,
210
211
  stdio: options.verbose ? "pipe" : ["pipe", "ignore", "pipe"],
211
212
  });
@@ -6,7 +6,7 @@ import { join } from "node:path";
6
6
  import { cliError } from "../error.js";
7
7
  import { isCandidCompatible } from "../helpers/is-candid-compatible.js";
8
8
  import { getWasmBindings } from "../wasm.js";
9
- import { readConfig } from "../mops.js";
9
+ import { getGlobalMocArgs, readConfig } from "../mops.js";
10
10
  import { sourcesArgs } from "./sources.js";
11
11
  import { toolchain } from "./toolchain/index.js";
12
12
  export const DEFAULT_BUILD_OUTPUT_DIR = ".mops/.build";
@@ -55,8 +55,8 @@ export async function build(canisterNames, options) {
55
55
  "-o",
56
56
  wasmPath,
57
57
  motokoPath,
58
- ...(options.extraArgs ?? []),
59
58
  ...(await sourcesArgs()).flat(),
59
+ ...getGlobalMocArgs(config),
60
60
  ];
61
61
  if (config.build?.args) {
62
62
  if (typeof config.build.args === "string") {
@@ -70,6 +70,7 @@ export async function build(canisterNames, options) {
70
70
  }
71
71
  args.push(...canister.args);
72
72
  }
73
+ args.push(...(options.extraArgs ?? []));
73
74
  const isPublicCandid = true; // always true for now to reduce corner cases
74
75
  const candidVisibility = isPublicCandid ? "icp:public" : "icp:private";
75
76
  if (isPublicCandid) {
@@ -2,6 +2,7 @@ import { relative } from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { execa } from "execa";
4
4
  import { cliError } from "../error.js";
5
+ import { getGlobalMocArgs, readConfig } from "../mops.js";
5
6
  import { autofixMotoko } from "../helpers/autofix-motoko.js";
6
7
  import { getMocSemVer } from "../helpers/get-moc-version.js";
7
8
  import { sourcesArgs } from "./sources.js";
@@ -16,8 +17,10 @@ export async function check(files, options = {}) {
16
17
  if (fileList.length === 0) {
17
18
  cliError("No Motoko files specified for checking");
18
19
  }
20
+ const config = readConfig();
19
21
  const mocPath = await toolchain.bin("moc", { fallback: true });
20
22
  const sources = await sourcesArgs();
23
+ const globalMocArgs = getGlobalMocArgs(config);
21
24
  // --all-libs enables richer diagnostics with edit suggestions from moc (requires moc >= 1.3.0)
22
25
  const allLibs = supportsAllLibsFlag();
23
26
  if (!allLibs) {
@@ -30,6 +33,7 @@ export async function check(files, options = {}) {
30
33
  "--check",
31
34
  ...(allLibs ? ["--all-libs"] : []),
32
35
  ...sources.flat(),
36
+ ...globalMocArgs,
33
37
  ...(options.extraArgs ?? []),
34
38
  ];
35
39
  if (options.fix) {
@@ -10,7 +10,7 @@ import chokidar from "chokidar";
10
10
  import debounce from "debounce";
11
11
  import { SemVer } from "semver";
12
12
  import { sources } from "../sources.js";
13
- import { getRootDir, readConfig } from "../../mops.js";
13
+ import { getGlobalMocArgs, getRootDir, readConfig } from "../../mops.js";
14
14
  import { parallel } from "../../parallel.js";
15
15
  import { MMF1 } from "./mmf1.js";
16
16
  import { absToRel, pipeMMF, pipeStderrToMMF, pipeStdoutToMMF, } from "./utils.js";
@@ -152,6 +152,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
152
152
  reporter.addFiles(files);
153
153
  let config = readConfig();
154
154
  let sourcesArr = await sources();
155
+ let globalMocArgs = getGlobalMocArgs(config);
155
156
  if (!mocPath) {
156
157
  mocPath = await toolchain.bin("moc", { fallback: true });
157
158
  }
@@ -197,6 +198,7 @@ export async function testWithReporter(reporterName, filter = "", defaultMode =
197
198
  "--hide-warnings",
198
199
  "--error-detail=2",
199
200
  ...sourcesArr.join(" ").split(" "),
201
+ ...globalMocArgs,
200
202
  file,
201
203
  ].filter((x) => x);
202
204
  // interpret
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
3
3
  import os from "node:os";
4
4
  import chalk from "chalk";
5
5
  import { getMocPath } from "../../helpers/get-moc-path.js";
6
- import { getRootDir } from "../../mops.js";
6
+ import { getGlobalMocArgs, getRootDir, readConfig } from "../../mops.js";
7
7
  import { sources } from "../sources.js";
8
8
  import { parallel } from "../../parallel.js";
9
9
  import { globMoFiles } from "./globMoFiles.js";
@@ -31,12 +31,18 @@ export class ErrorChecker {
31
31
  let rootDir = getRootDir();
32
32
  let mocPath = getMocPath();
33
33
  let deps = await sources({ cwd: rootDir });
34
+ let globalMocArgs = getGlobalMocArgs(readConfig());
34
35
  let paths = globMoFiles(rootDir);
35
36
  this.totalFiles = paths.length;
36
37
  this.processedFiles = 0;
37
38
  await parallel(os.cpus().length, paths, async (file) => {
38
39
  try {
39
- await promisify(execFile)(mocPath, ["--check", ...deps.flatMap((x) => x.split(" ")), file], { cwd: rootDir });
40
+ await promisify(execFile)(mocPath, [
41
+ "--check",
42
+ ...deps.flatMap((x) => x.split(" ")),
43
+ ...globalMocArgs,
44
+ file,
45
+ ], { cwd: rootDir });
40
46
  }
41
47
  catch (error) {
42
48
  error.message.split("\n").forEach((line) => {
@@ -3,7 +3,7 @@ import { promisify } from "node:util";
3
3
  import os from "node:os";
4
4
  import chalk from "chalk";
5
5
  import { getMocPath } from "../../helpers/get-moc-path.js";
6
- import { getRootDir } from "../../mops.js";
6
+ import { getGlobalMocArgs, getRootDir, readConfig } from "../../mops.js";
7
7
  import { sources } from "../sources.js";
8
8
  import { parallel } from "../../parallel.js";
9
9
  import { globMoFiles } from "./globMoFiles.js";
@@ -51,6 +51,7 @@ export class WarningChecker {
51
51
  let rootDir = getRootDir();
52
52
  let mocPath = getMocPath();
53
53
  let deps = await sources({ cwd: rootDir });
54
+ let globalMocArgs = getGlobalMocArgs(readConfig());
54
55
  let paths = globMoFiles(rootDir);
55
56
  this.totalFiles = paths.length;
56
57
  this.processedFiles = 0;
@@ -58,7 +59,12 @@ export class WarningChecker {
58
59
  let controller = new AbortController();
59
60
  let { signal } = controller;
60
61
  this.controllers.set(file, controller);
61
- let { stderr } = await promisify(execFile)(mocPath, ["--check", ...deps.flatMap((x) => x.split(" ")), file], { cwd: rootDir, signal }).catch((error) => {
62
+ let { stderr } = await promisify(execFile)(mocPath, [
63
+ "--check",
64
+ ...deps.flatMap((x) => x.split(" ")),
65
+ ...globalMocArgs,
66
+ file,
67
+ ], { cwd: rootDir, signal }).catch((error) => {
62
68
  if (error.code === "ABORT_ERR") {
63
69
  return { stderr: "" };
64
70
  }
package/dist/error.d.ts CHANGED
@@ -1 +1 @@
1
- export declare function cliError(...args: unknown[]): void;
1
+ export declare function cliError(...args: unknown[]): never;
@@ -16,75 +16,120 @@ export function parseDiagnostics(stdout) {
16
16
  })
17
17
  .filter((d) => d !== null);
18
18
  }
19
- function extractFixes(diagnostics) {
20
- const fixes = [];
19
+ function extractDiagnosticFixes(diagnostics) {
20
+ const result = new Map();
21
21
  for (const diag of diagnostics) {
22
+ const editsByFile = new Map();
22
23
  for (const span of diag.spans) {
23
24
  if (span.suggestion_applicability === "MachineApplicable" &&
24
25
  span.suggested_replacement !== null) {
25
- fixes.push({
26
- file: span.file,
27
- code: diag.code,
28
- edit: {
29
- range: {
30
- start: {
31
- line: span.line_start - 1,
32
- character: span.column_start - 1,
33
- },
34
- end: {
35
- line: span.line_end - 1,
36
- character: span.column_end - 1,
37
- },
26
+ const file = resolve(span.file);
27
+ const edits = editsByFile.get(file) ?? [];
28
+ edits.push({
29
+ range: {
30
+ start: {
31
+ line: span.line_start - 1,
32
+ character: span.column_start - 1,
33
+ },
34
+ end: {
35
+ line: span.line_end - 1,
36
+ character: span.column_end - 1,
38
37
  },
39
- newText: span.suggested_replacement,
40
38
  },
39
+ newText: span.suggested_replacement,
41
40
  });
41
+ editsByFile.set(file, edits);
42
42
  }
43
43
  }
44
+ for (const [file, edits] of editsByFile) {
45
+ const existing = result.get(file) ?? [];
46
+ existing.push({ code: diag.code, edits });
47
+ result.set(file, existing);
48
+ }
44
49
  }
45
- return fixes;
50
+ return result;
51
+ }
52
+ function normalizeRange(range) {
53
+ const { start, end } = range;
54
+ if (start.line > end.line ||
55
+ (start.line === end.line && start.character > end.character)) {
56
+ return { start: end, end: start };
57
+ }
58
+ return range;
59
+ }
60
+ /**
61
+ * Applies diagnostic fixes to a document, processing each diagnostic as
62
+ * an atomic unit. If any edit from a diagnostic overlaps with an already-accepted
63
+ * edit, the entire diagnostic is skipped (picked up in subsequent iterations).
64
+ * Based on vscode-languageserver-textdocument's TextDocument.applyEdits.
65
+ */
66
+ function applyDiagnosticFixes(doc, fixes) {
67
+ const acceptedEdits = [];
68
+ const appliedCodes = [];
69
+ for (const fix of fixes) {
70
+ const offsets = fix.edits.map((e) => {
71
+ const range = normalizeRange(e.range);
72
+ return {
73
+ start: doc.offsetAt(range.start),
74
+ end: doc.offsetAt(range.end),
75
+ newText: e.newText,
76
+ };
77
+ });
78
+ const overlaps = offsets.some((o) => acceptedEdits.some((a) => o.start < a.end && o.end > a.start));
79
+ if (overlaps) {
80
+ continue;
81
+ }
82
+ acceptedEdits.push(...offsets);
83
+ appliedCodes.push(fix.code);
84
+ }
85
+ acceptedEdits.sort((a, b) => a.start - b.start);
86
+ const text = doc.getText();
87
+ const spans = [];
88
+ let lastOffset = 0;
89
+ for (const edit of acceptedEdits) {
90
+ if (edit.start < lastOffset) {
91
+ continue;
92
+ }
93
+ if (edit.start > lastOffset) {
94
+ spans.push(text.substring(lastOffset, edit.start));
95
+ }
96
+ if (edit.newText.length) {
97
+ spans.push(edit.newText);
98
+ }
99
+ lastOffset = edit.end;
100
+ }
101
+ spans.push(text.substring(lastOffset));
102
+ return { text: spans.join(""), appliedCodes };
46
103
  }
47
104
  const MAX_FIX_ITERATIONS = 10;
48
105
  export async function autofixMotoko(mocPath, files, mocArgs) {
49
106
  const fixedFilesCodes = new Map();
50
107
  for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
51
- const allFixes = [];
108
+ const fixesByFile = new Map();
52
109
  for (const file of files) {
53
- const result = await execa(mocPath, [file, "--error-format=json", ...mocArgs], { stdio: "pipe", reject: false });
110
+ const result = await execa(mocPath, [file, ...mocArgs, "--error-format=json"], { stdio: "pipe", reject: false });
54
111
  const diagnostics = parseDiagnostics(result.stdout);
55
- allFixes.push(...extractFixes(diagnostics));
112
+ for (const [targetFile, fixes] of extractDiagnosticFixes(diagnostics)) {
113
+ const existing = fixesByFile.get(targetFile) ?? [];
114
+ existing.push(...fixes);
115
+ fixesByFile.set(targetFile, existing);
116
+ }
56
117
  }
57
- if (allFixes.length === 0) {
118
+ if (fixesByFile.size === 0) {
58
119
  break;
59
120
  }
60
- const fixesByFile = new Map();
61
- for (const fix of allFixes) {
62
- const normalizedPath = resolve(fix.file);
63
- const existing = fixesByFile.get(normalizedPath) ?? [];
64
- existing.push(fix);
65
- fixesByFile.set(normalizedPath, existing);
66
- }
67
121
  let progress = false;
68
122
  for (const [file, fixes] of fixesByFile) {
69
123
  const original = await readFile(file, "utf-8");
70
124
  const doc = TextDocument.create(`file://${file}`, "motoko", 0, original);
71
- let result;
72
- try {
73
- result = TextDocument.applyEdits(doc, fixes.map((f) => f.edit));
74
- }
75
- catch (err) {
76
- console.warn(`Warning: could not apply fixes to ${file}: ${err}`);
77
- continue;
78
- }
125
+ const { text: result, appliedCodes } = applyDiagnosticFixes(doc, fixes);
79
126
  if (result === original) {
80
127
  continue;
81
128
  }
82
129
  await writeFile(file, result, "utf-8");
83
130
  progress = true;
84
131
  const existing = fixedFilesCodes.get(file) ?? [];
85
- for (const fix of fixes) {
86
- existing.push(fix.code);
87
- }
132
+ existing.push(...appliedCodes);
88
133
  fixedFilesCodes.set(file, existing);
89
134
  }
90
135
  if (!progress) {
package/dist/mops.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare function getGithubCommit(repo: string, ref: string): Promise<any>
23
23
  export declare function getDependencyType(version: string): "local" | "mops" | "github";
24
24
  export declare function parseDepValue(name: string, value: string): Dependency;
25
25
  export declare function readConfig(configFile?: string): Config;
26
+ export declare function getGlobalMocArgs(config: Config): string[];
26
27
  export declare function writeConfig(config: Config, configFile?: string): void;
27
28
  export declare function formatDir(name: string, version: string): string;
28
29
  export declare function formatGithubDir(name: string, repo: string): string;
package/dist/mops.js CHANGED
@@ -6,6 +6,7 @@ import chalk from "chalk";
6
6
  import prompts from "prompts";
7
7
  import fetch from "node-fetch";
8
8
  import { decodeFile } from "./pem.js";
9
+ import { cliError } from "./error.js";
9
10
  import { mainActor, storageActor } from "./api/actors.js";
10
11
  import { getNetwork } from "./api/network.js";
11
12
  import { getHighestVersion } from "./api/getHighestVersion.js";
@@ -177,6 +178,15 @@ export function readConfig(configFile = getClosestConfigFile()) {
177
178
  });
178
179
  return config;
179
180
  }
181
+ export function getGlobalMocArgs(config) {
182
+ if (!config.moc?.args) {
183
+ return [];
184
+ }
185
+ if (typeof config.moc.args === "string") {
186
+ cliError(`[moc] config 'args' should be an array of strings in mops.toml config file`);
187
+ }
188
+ return config.moc.args;
189
+ }
180
190
  export function writeConfig(config, configFile = getClosestConfigFile()) {
181
191
  let resConfig = JSON.parse(JSON.stringify(config));
182
192
  let deps = resConfig.dependencies || {};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "bin/mops.js",
@@ -52,6 +52,9 @@ describe("check --fix", () => {
52
52
  M0237: 17,
53
53
  });
54
54
  });
55
+ test("overlapping edits", async () => {
56
+ await testCheckFix("overlapping.mo", { M0223: 1, M0236: 2 });
57
+ });
55
58
  test("transitive imports", async () => {
56
59
  const runMainPath = copyFixture("transitive-main.mo");
57
60
  const runLibPath = copyFixture("transitive-lib.mo");
@@ -62,6 +65,19 @@ describe("check --fix", () => {
62
65
  const afterResult = await cli(["check", runMainPath, "--", ...diagnosticFlags], { cwd: fixDir });
63
66
  expect(countCodes(afterResult.stdout)).toEqual({});
64
67
  });
68
+ test("--error-format=human does not break --fix", async () => {
69
+ const runFilePath = copyFixture("M0223.mo");
70
+ const fixResult = await cli([
71
+ "check",
72
+ runFilePath,
73
+ "--fix",
74
+ "--",
75
+ warningFlags,
76
+ "--error-format=human",
77
+ ], { cwd: fixDir });
78
+ expect(fixResult.stdout).toContain("1 fix applied");
79
+ expect(readFileSync(runFilePath, "utf-8")).not.toContain("<Nat>");
80
+ });
65
81
  test("verbose", async () => {
66
82
  const result = await cli(["check", "Ok.mo", "--fix", "--verbose"], {
67
83
  cwd: fixDir,
@@ -30,4 +30,8 @@ describe("check", () => {
30
30
  expect(result.stderr).toMatch(/warning \[M0194\]/);
31
31
  expect(result.stderr).toMatch(/unused identifier/);
32
32
  });
33
+ test("[moc] args are passed to moc", async () => {
34
+ const cwd = path.join(import.meta.dirname, "check/moc-args");
35
+ await cliSnapshot(["check", "Warning.mo"], { cwd }, 1);
36
+ });
33
37
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import path from "path";
3
+ import { cli } from "./helpers";
4
+ describe("moc-args", () => {
5
+ test("prints moc args from [moc] config", async () => {
6
+ const cwd = path.join(import.meta.dirname, "check/moc-args");
7
+ const result = await cli(["moc-args"], { cwd });
8
+ expect(result.exitCode).toBe(0);
9
+ expect(result.stdout).toBe("-Werror");
10
+ });
11
+ test("prints nothing when no [moc] config", async () => {
12
+ const cwd = path.join(import.meta.dirname, "check/success");
13
+ const result = await cli(["moc-args"], { cwd });
14
+ expect(result.exitCode).toBe(0);
15
+ expect(result.stdout).toBe("");
16
+ });
17
+ });
package/dist/types.d.ts CHANGED
@@ -19,6 +19,9 @@ export type Config = {
19
19
  "dev-dependencies"?: Dependencies;
20
20
  toolchain?: Toolchain;
21
21
  requirements?: Requirements;
22
+ moc?: {
23
+ args?: string[];
24
+ };
22
25
  canisters?: Record<string, string | CanisterConfig>;
23
26
  build?: {
24
27
  outputDir?: string;
package/error.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
 
3
- export function cliError(...args: unknown[]) {
3
+ export function cliError(...args: unknown[]): never {
4
4
  console.error(chalk.red(...args));
5
5
  process.exit(1);
6
6
  }
@@ -6,12 +6,6 @@ import {
6
6
  type TextEdit,
7
7
  } from "vscode-languageserver-textdocument";
8
8
 
9
- interface Fix {
10
- file: string;
11
- code: string;
12
- edit: TextEdit;
13
- }
14
-
15
9
  interface MocSpan {
16
10
  file: string;
17
11
  line_start: number;
@@ -46,35 +40,127 @@ export function parseDiagnostics(stdout: string): MocDiagnostic[] {
46
40
  .filter((d) => d !== null);
47
41
  }
48
42
 
49
- function extractFixes(diagnostics: MocDiagnostic[]): Fix[] {
50
- const fixes: Fix[] = [];
43
+ interface DiagnosticFix {
44
+ code: string;
45
+ edits: TextEdit[];
46
+ }
47
+
48
+ function extractDiagnosticFixes(
49
+ diagnostics: MocDiagnostic[],
50
+ ): Map<string, DiagnosticFix[]> {
51
+ const result = new Map<string, DiagnosticFix[]>();
52
+
51
53
  for (const diag of diagnostics) {
54
+ const editsByFile = new Map<string, TextEdit[]>();
55
+
52
56
  for (const span of diag.spans) {
53
57
  if (
54
58
  span.suggestion_applicability === "MachineApplicable" &&
55
59
  span.suggested_replacement !== null
56
60
  ) {
57
- fixes.push({
58
- file: span.file,
59
- code: diag.code,
60
- edit: {
61
- range: {
62
- start: {
63
- line: span.line_start - 1,
64
- character: span.column_start - 1,
65
- },
66
- end: {
67
- line: span.line_end - 1,
68
- character: span.column_end - 1,
69
- },
61
+ const file = resolve(span.file);
62
+ const edits = editsByFile.get(file) ?? [];
63
+ edits.push({
64
+ range: {
65
+ start: {
66
+ line: span.line_start - 1,
67
+ character: span.column_start - 1,
68
+ },
69
+ end: {
70
+ line: span.line_end - 1,
71
+ character: span.column_end - 1,
70
72
  },
71
- newText: span.suggested_replacement,
72
73
  },
74
+ newText: span.suggested_replacement,
73
75
  });
76
+ editsByFile.set(file, edits);
74
77
  }
75
78
  }
79
+
80
+ for (const [file, edits] of editsByFile) {
81
+ const existing = result.get(file) ?? [];
82
+ existing.push({ code: diag.code, edits });
83
+ result.set(file, existing);
84
+ }
85
+ }
86
+
87
+ return result;
88
+ }
89
+
90
+ type Range = TextEdit["range"];
91
+
92
+ function normalizeRange(range: Range): Range {
93
+ const { start, end } = range;
94
+ if (
95
+ start.line > end.line ||
96
+ (start.line === end.line && start.character > end.character)
97
+ ) {
98
+ return { start: end, end: start };
99
+ }
100
+ return range;
101
+ }
102
+
103
+ interface OffsetEdit {
104
+ start: number;
105
+ end: number;
106
+ newText: string;
107
+ }
108
+
109
+ /**
110
+ * Applies diagnostic fixes to a document, processing each diagnostic as
111
+ * an atomic unit. If any edit from a diagnostic overlaps with an already-accepted
112
+ * edit, the entire diagnostic is skipped (picked up in subsequent iterations).
113
+ * Based on vscode-languageserver-textdocument's TextDocument.applyEdits.
114
+ */
115
+ function applyDiagnosticFixes(
116
+ doc: TextDocument,
117
+ fixes: DiagnosticFix[],
118
+ ): { text: string; appliedCodes: string[] } {
119
+ const acceptedEdits: OffsetEdit[] = [];
120
+ const appliedCodes: string[] = [];
121
+
122
+ for (const fix of fixes) {
123
+ const offsets: OffsetEdit[] = fix.edits.map((e) => {
124
+ const range = normalizeRange(e.range);
125
+ return {
126
+ start: doc.offsetAt(range.start),
127
+ end: doc.offsetAt(range.end),
128
+ newText: e.newText,
129
+ };
130
+ });
131
+
132
+ const overlaps = offsets.some((o) =>
133
+ acceptedEdits.some((a) => o.start < a.end && o.end > a.start),
134
+ );
135
+ if (overlaps) {
136
+ continue;
137
+ }
138
+
139
+ acceptedEdits.push(...offsets);
140
+ appliedCodes.push(fix.code);
141
+ }
142
+
143
+ acceptedEdits.sort((a, b) => a.start - b.start);
144
+
145
+ const text = doc.getText();
146
+ const spans: string[] = [];
147
+ let lastOffset = 0;
148
+
149
+ for (const edit of acceptedEdits) {
150
+ if (edit.start < lastOffset) {
151
+ continue;
152
+ }
153
+ if (edit.start > lastOffset) {
154
+ spans.push(text.substring(lastOffset, edit.start));
155
+ }
156
+ if (edit.newText.length) {
157
+ spans.push(edit.newText);
158
+ }
159
+ lastOffset = edit.end;
76
160
  }
77
- return fixes;
161
+
162
+ spans.push(text.substring(lastOffset));
163
+ return { text: spans.join(""), appliedCodes };
78
164
  }
79
165
 
80
166
  const MAX_FIX_ITERATIONS = 10;
@@ -93,47 +179,33 @@ export async function autofixMotoko(
93
179
  const fixedFilesCodes = new Map<string, string[]>();
94
180
 
95
181
  for (let iteration = 0; iteration < MAX_FIX_ITERATIONS; iteration++) {
96
- const allFixes: Fix[] = [];
182
+ const fixesByFile = new Map<string, DiagnosticFix[]>();
97
183
 
98
184
  for (const file of files) {
99
185
  const result = await execa(
100
186
  mocPath,
101
- [file, "--error-format=json", ...mocArgs],
187
+ [file, ...mocArgs, "--error-format=json"],
102
188
  { stdio: "pipe", reject: false },
103
189
  );
104
190
 
105
191
  const diagnostics = parseDiagnostics(result.stdout);
106
- allFixes.push(...extractFixes(diagnostics));
192
+ for (const [targetFile, fixes] of extractDiagnosticFixes(diagnostics)) {
193
+ const existing = fixesByFile.get(targetFile) ?? [];
194
+ existing.push(...fixes);
195
+ fixesByFile.set(targetFile, existing);
196
+ }
107
197
  }
108
198
 
109
- if (allFixes.length === 0) {
199
+ if (fixesByFile.size === 0) {
110
200
  break;
111
201
  }
112
202
 
113
- const fixesByFile = new Map<string, Fix[]>();
114
- for (const fix of allFixes) {
115
- const normalizedPath = resolve(fix.file);
116
- const existing = fixesByFile.get(normalizedPath) ?? [];
117
- existing.push(fix);
118
- fixesByFile.set(normalizedPath, existing);
119
- }
120
-
121
203
  let progress = false;
122
204
 
123
205
  for (const [file, fixes] of fixesByFile) {
124
206
  const original = await readFile(file, "utf-8");
125
207
  const doc = TextDocument.create(`file://${file}`, "motoko", 0, original);
126
-
127
- let result: string;
128
- try {
129
- result = TextDocument.applyEdits(
130
- doc,
131
- fixes.map((f) => f.edit),
132
- );
133
- } catch (err) {
134
- console.warn(`Warning: could not apply fixes to ${file}: ${err}`);
135
- continue;
136
- }
208
+ const { text: result, appliedCodes } = applyDiagnosticFixes(doc, fixes);
137
209
 
138
210
  if (result === original) {
139
211
  continue;
@@ -143,9 +215,7 @@ export async function autofixMotoko(
143
215
  progress = true;
144
216
 
145
217
  const existing = fixedFilesCodes.get(file) ?? [];
146
- for (const fix of fixes) {
147
- existing.push(fix.code);
148
- }
218
+ existing.push(...appliedCodes);
149
219
  fixedFilesCodes.set(file, existing);
150
220
  }
151
221
 
package/mops.ts CHANGED
@@ -8,6 +8,7 @@ import prompts from "prompts";
8
8
  import fetch from "node-fetch";
9
9
 
10
10
  import { decodeFile } from "./pem.js";
11
+ import { cliError } from "./error.js";
11
12
  import { Config, Dependency } from "./types.js";
12
13
  import { mainActor, storageActor } from "./api/actors.js";
13
14
  import { getNetwork } from "./api/network.js";
@@ -204,6 +205,18 @@ export function readConfig(configFile = getClosestConfigFile()): Config {
204
205
  return config;
205
206
  }
206
207
 
208
+ export function getGlobalMocArgs(config: Config): string[] {
209
+ if (!config.moc?.args) {
210
+ return [];
211
+ }
212
+ if (typeof config.moc.args === "string") {
213
+ cliError(
214
+ `[moc] config 'args' should be an array of strings in mops.toml config file`,
215
+ );
216
+ }
217
+ return config.moc.args;
218
+ }
219
+
207
220
  export function writeConfig(
208
221
  config: Config,
209
222
  configFile = getClosestConfigFile(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ic-mops",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mops": "dist/bin/mops.js",
@@ -37,9 +37,9 @@ persistent actor {
37
37
  `;
38
38
 
39
39
  exports[`check --fix M0236: fix output 1`] = `
40
- "Fixed run/M0236.mo (2 fixes: M0236)
40
+ "Fixed run/M0236.mo (1 fix: M0236)
41
41
 
42
- 2 fixes applied to 1 file"
42
+ 1 fix applied to 1 file"
43
43
  `;
44
44
 
45
45
  exports[`check --fix M0237 1`] = `
@@ -203,16 +203,35 @@ do {
203
203
  `;
204
204
 
205
205
  exports[`check --fix edit-suggestions: fix output 1`] = `
206
- "Fixed run/edit-suggestions.mo (41 fixes: M0223, M0236, M0237)
206
+ "Fixed run/edit-suggestions.mo (30 fixes: M0223, M0236, M0237)
207
+
208
+ ✓ 30 fixes applied to 1 file"
209
+ `;
210
+
211
+ exports[`check --fix overlapping edits 1`] = `
212
+ "import Array "mo:core/Array";
213
+
214
+ // Overlapping fixable errors (nested calls produce overlapping M0223 + M0236 edits)
215
+ do {
216
+ let ar = [1, 2, 3];
217
+ let _ = ar.filter(func(x) { x > 0 }).filter(
218
+ func(x) { x > 0 },
219
+ );
220
+ };
221
+ "
222
+ `;
223
+
224
+ exports[`check --fix overlapping edits: fix output 1`] = `
225
+ "Fixed run/overlapping.mo (4 fixes: M0223, M0236)
207
226
 
208
- 41 fixes applied to 1 file"
227
+ 4 fixes applied to 1 file"
209
228
  `;
210
229
 
211
230
  exports[`check --fix transitive imports: fix output 1`] = `
212
- "Fixed run/transitive-lib.mo (2 fixes: M0236)
231
+ "Fixed run/transitive-lib.mo (1 fix: M0236)
213
232
  Fixed run/transitive-main.mo (1 fix: M0223)
214
233
 
215
- 3 fixes applied to 2 files"
234
+ 2 fixes applied to 2 files"
216
235
  `;
217
236
 
218
237
  exports[`check --fix transitive imports: lib file 1`] = `
@@ -1,5 +1,14 @@
1
1
  // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
+ exports[`check [moc] args are passed to moc 1`] = `
4
+ {
5
+ "exitCode": 1,
6
+ "stderr": "Warning.mo:3.9-3.15: warning [M0194], unused identifier unused (delete or rename to wildcard \`_\` or \`_unused\`)
7
+ ✗ Check failed for file Warning.mo (exit code: 1)",
8
+ "stdout": "moc < 1.3.0: some diagnostic hints may be missing",
9
+ }
10
+ `;
11
+
3
12
  exports[`check error 1`] = `
4
13
  {
5
14
  "exitCode": 1,
@@ -0,0 +1,10 @@
1
+ import Array "mo:core/Array";
2
+
3
+ // Overlapping fixable errors (nested calls produce overlapping M0223 + M0236 edits)
4
+ do {
5
+ let ar = [1, 2, 3];
6
+ let _ = Array.filter<Nat>(
7
+ Array.filter<Nat>(ar, func(x) { x > 0 }),
8
+ func(x) { x > 0 },
9
+ );
10
+ };
@@ -0,0 +1,5 @@
1
+ persistent actor {
2
+ public func example() : async () {
3
+ let unused = 123;
4
+ };
5
+ };
@@ -0,0 +1,2 @@
1
+ [moc]
2
+ args = ["-Werror"]
@@ -80,6 +80,10 @@ describe("check --fix", () => {
80
80
  });
81
81
  });
82
82
 
83
+ test("overlapping edits", async () => {
84
+ await testCheckFix("overlapping.mo", { M0223: 1, M0236: 2 });
85
+ });
86
+
83
87
  test("transitive imports", async () => {
84
88
  const runMainPath = copyFixture("transitive-main.mo");
85
89
  const runLibPath = copyFixture("transitive-lib.mo");
@@ -100,6 +104,25 @@ describe("check --fix", () => {
100
104
  expect(countCodes(afterResult.stdout)).toEqual({});
101
105
  });
102
106
 
107
+ test("--error-format=human does not break --fix", async () => {
108
+ const runFilePath = copyFixture("M0223.mo");
109
+
110
+ const fixResult = await cli(
111
+ [
112
+ "check",
113
+ runFilePath,
114
+ "--fix",
115
+ "--",
116
+ warningFlags,
117
+ "--error-format=human",
118
+ ],
119
+ { cwd: fixDir },
120
+ );
121
+
122
+ expect(fixResult.stdout).toContain("1 fix applied");
123
+ expect(readFileSync(runFilePath, "utf-8")).not.toContain("<Nat>");
124
+ });
125
+
103
126
  test("verbose", async () => {
104
127
  const result = await cli(["check", "Ok.mo", "--fix", "--verbose"], {
105
128
  cwd: fixDir,
@@ -43,4 +43,9 @@ describe("check", () => {
43
43
  expect(result.stderr).toMatch(/warning \[M0194\]/);
44
44
  expect(result.stderr).toMatch(/unused identifier/);
45
45
  });
46
+
47
+ test("[moc] args are passed to moc", async () => {
48
+ const cwd = path.join(import.meta.dirname, "check/moc-args");
49
+ await cliSnapshot(["check", "Warning.mo"], { cwd }, 1);
50
+ });
46
51
  });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import path from "path";
3
+ import { cli } from "./helpers";
4
+
5
+ describe("moc-args", () => {
6
+ test("prints moc args from [moc] config", async () => {
7
+ const cwd = path.join(import.meta.dirname, "check/moc-args");
8
+ const result = await cli(["moc-args"], { cwd });
9
+ expect(result.exitCode).toBe(0);
10
+ expect(result.stdout).toBe("-Werror");
11
+ });
12
+
13
+ test("prints nothing when no [moc] config", async () => {
14
+ const cwd = path.join(import.meta.dirname, "check/success");
15
+ const result = await cli(["moc-args"], { cwd });
16
+ expect(result.exitCode).toBe(0);
17
+ expect(result.stdout).toBe("");
18
+ });
19
+ });
package/types.ts CHANGED
@@ -19,6 +19,9 @@ export type Config = {
19
19
  "dev-dependencies"?: Dependencies;
20
20
  toolchain?: Toolchain;
21
21
  requirements?: Requirements;
22
+ moc?: {
23
+ args?: string[];
24
+ };
22
25
  canisters?: Record<string, string | CanisterConfig>;
23
26
  build?: {
24
27
  outputDir?: string;