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.
- package/CHANGELOG.md +5 -0
- package/RELEASE.md +19 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +15 -0
- package/commands/bench.ts +11 -3
- package/commands/build.ts +3 -2
- package/commands/check.ts +4 -0
- package/commands/test/test.ts +3 -1
- package/commands/watch/error-checker.ts +8 -2
- package/commands/watch/warning-checker.ts +8 -2
- package/dist/cli.js +13 -1
- package/dist/commands/bench.js +5 -4
- package/dist/commands/build.js +3 -2
- package/dist/commands/check.js +4 -0
- package/dist/commands/test/test.js +3 -1
- package/dist/commands/watch/error-checker.js +8 -2
- package/dist/commands/watch/warning-checker.js +8 -2
- package/dist/error.d.ts +1 -1
- package/dist/helpers/autofix-motoko.js +84 -39
- package/dist/mops.d.ts +1 -0
- package/dist/mops.js +10 -0
- package/dist/package.json +1 -1
- package/dist/tests/check-fix.test.js +16 -0
- package/dist/tests/check.test.js +4 -0
- package/dist/tests/moc-args.test.d.ts +1 -0
- package/dist/tests/moc-args.test.js +17 -0
- package/dist/types.d.ts +3 -0
- package/error.ts +1 -1
- package/helpers/autofix-motoko.ts +119 -49
- package/mops.ts +13 -0
- package/package.json +1 -1
- package/tests/__snapshots__/check-fix.test.ts.snap +25 -6
- package/tests/__snapshots__/check.test.ts.snap +9 -0
- package/tests/check/fix/overlapping.mo +10 -0
- package/tests/check/moc-args/Warning.mo +5 -0
- package/tests/check/moc-args/mops.toml +2 -0
- package/tests/check-fix.test.ts +23 -0
- package/tests/check.test.ts +5 -0
- package/tests/moc-args.test.ts +19 -0
- 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 {
|
|
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
|
|
package/commands/test/test.ts
CHANGED
|
@@ -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
|
-
[
|
|
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
|
-
[
|
|
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>")
|
package/dist/commands/bench.js
CHANGED
|
@@ -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
|
});
|
package/dist/commands/build.js
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 { 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) {
|
package/dist/commands/check.js
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";
|
|
@@ -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, [
|
|
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, [
|
|
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[]):
|
|
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
|
|
20
|
-
const
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
108
|
+
const fixesByFile = new Map();
|
|
52
109
|
for (const file of files) {
|
|
53
|
-
const result = await execa(mocPath, [file, "--error-format=json"
|
|
110
|
+
const result = await execa(mocPath, [file, ...mocArgs, "--error-format=json"], { stdio: "pipe", reject: false });
|
|
54
111
|
const diagnostics = parseDiagnostics(result.stdout);
|
|
55
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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,
|
package/dist/tests/check.test.js
CHANGED
|
@@ -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
package/error.ts
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
187
|
+
[file, ...mocArgs, "--error-format=json"],
|
|
102
188
|
{ stdio: "pipe", reject: false },
|
|
103
189
|
);
|
|
104
190
|
|
|
105
191
|
const diagnostics = parseDiagnostics(result.stdout);
|
|
106
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
@@ -37,9 +37,9 @@ persistent actor {
|
|
|
37
37
|
`;
|
|
38
38
|
|
|
39
39
|
exports[`check --fix M0236: fix output 1`] = `
|
|
40
|
-
"Fixed run/M0236.mo (
|
|
40
|
+
"Fixed run/M0236.mo (1 fix: M0236)
|
|
41
41
|
|
|
42
|
-
✓
|
|
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 (
|
|
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
|
-
✓
|
|
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 (
|
|
231
|
+
"Fixed run/transitive-lib.mo (1 fix: M0236)
|
|
213
232
|
Fixed run/transitive-main.mo (1 fix: M0223)
|
|
214
233
|
|
|
215
|
-
✓
|
|
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,
|
package/tests/check-fix.test.ts
CHANGED
|
@@ -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,
|
package/tests/check.test.ts
CHANGED
|
@@ -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