Skip to content

Commit b0b5264

Browse files
feat: Detect unused permissions in Snaps CLI (#3335)
This adds unused permission detection for handlers to the Snaps CLI. It will detect two cases: - The Snap uses a certain permission, but does not export a handler for it. - The Snap exports a handler, but does not request permission for it. Unfortunately due to how it works, it did require some refactors to the CLI: - The eval step in the build commands was removed in favour of the Webpack plugin. Having a separate step made it complicated to support things like watch mode, since that's handled by Webpack. - Eval must run in order to detect unused permissions, so it's now run in the `manifest` command as well. --------- Co-authored-by: Frederik Bolding <[email protected]>
1 parent e101e0f commit b0b5264

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+710
-157
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ linkStyle default opacity:0.5
4545
snaps_utils(["@metamask/snaps-utils"]);
4646
snaps_webpack_plugin(["@metamask/snaps-webpack-plugin"]);
4747
create_snap --> snaps_utils;
48+
snaps_cli --> snaps_rpc_methods;
4849
snaps_cli --> snaps_sandbox;
4950
snaps_cli --> snaps_sdk;
5051
snaps_cli --> snaps_utils;
@@ -67,6 +68,7 @@ linkStyle default opacity:0.5
6768
snaps_simulation --> snaps_sdk;
6869
snaps_simulation --> snaps_utils;
6970
snaps_utils --> snaps_sdk;
71+
snaps_webpack_plugin --> snaps_rpc_methods;
7072
snaps_webpack_plugin --> snaps_sdk;
7173
snaps_webpack_plugin --> snaps_utils;
7274
```

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const config = createConfig([
1414
'**/docs',
1515
'**/public',
1616
'.yarn',
17+
'!packages/snaps-cli/src/commands/build',
1718
],
1819
},
1920

packages/snaps-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"test:watch": "jest --watch"
6565
},
6666
"dependencies": {
67+
"@metamask/snaps-rpc-methods": "workspace:^",
6768
"@metamask/snaps-sandbox": "workspace:^",
6869
"@metamask/snaps-sdk": "workspace:^",
6970
"@metamask/snaps-utils": "workspace:^",

packages/snaps-cli/src/__mocks__/ora.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ class MockSpinner {
99

1010
fail = jest.fn();
1111

12-
stop = jest.fn();
12+
stop = jest.fn().mockImplementation(() => {
13+
this.isSpinning = false;
14+
});
1315

1416
clear = jest.fn();
1517

packages/snaps-cli/src/builders.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const builders = {
1919
normalize: true,
2020
},
2121

22+
eval: {
23+
describe: 'Evaluate the Snap bundle',
24+
type: 'boolean',
25+
},
26+
2227
fix: {
2328
describe: 'Attempt to fix snap.manifest.json',
2429
type: 'boolean',

packages/snaps-cli/src/commands/build/build.e2e.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { promises as fs } from 'fs';
2-
import { join } from 'path';
3-
41
import type { TestRunner } from '../../test-utils';
52
import { getCommandRunner } from '../../test-utils';
63

@@ -21,8 +18,9 @@ describe('mm-snap build', () => {
2118
expect.stringMatching(/Checking the input file\./u),
2219
);
2320
expect(runner.stdout).toContainEqual(
24-
expect.stringMatching(/Building the snap bundle\./u),
21+
expect.stringMatching(/Building the Snap bundle\./u),
2522
);
23+
2624
expect(runner.stderr).toContainEqual(
2725
expect.stringMatching(
2826
/Compiled \d+ files? in \d+ms with \d+ warnings?\./u,
@@ -33,9 +31,6 @@ describe('mm-snap build', () => {
3331
'No icon found in the Snap manifest. It is recommended to include an icon for the Snap. See https://docs.metamask.io/snaps/how-to/design-a-snap/#guidelines-at-a-glance for more information.',
3432
),
3533
);
36-
expect(runner.stdout).toContainEqual(
37-
expect.stringMatching(/Evaluating the snap bundle\./u),
38-
);
3934
expect(runner.exitCode).toBe(0);
4035
},
4136
);

packages/snaps-cli/src/commands/build/build.test.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
66
import { buildHandler } from './build';
77
import { build } from './implementation';
88
import { getMockConfig } from '../../test-utils';
9-
import { evaluate } from '../eval';
109

1110
jest.mock('fs');
12-
jest.mock('../eval');
1311
jest.mock('./implementation');
1412

1513
jest.mock('webpack-bundle-analyzer', () => ({
1614
BundleAnalyzerPlugin: jest.fn(),
1715
}));
1816

1917
describe('buildHandler', () => {
20-
it('builds a snap', async () => {
18+
it('builds a Snap', async () => {
2119
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);
2220

2321
jest.spyOn(console, 'log').mockImplementation();
@@ -34,16 +32,12 @@ describe('buildHandler', () => {
3432
expect(process.exitCode).not.toBe(1);
3533
expect(build).toHaveBeenCalledWith(config, {
3634
analyze: false,
37-
evaluate: false,
35+
evaluate: true,
3836
spinner: expect.any(Object),
3937
});
40-
41-
expect(evaluate).toHaveBeenCalledWith(
42-
expect.stringMatching(/.*output\.js.*/u),
43-
);
4438
});
4539

46-
it('analyzes a snap bundle', async () => {
40+
it('analyzes a Snap bundle', async () => {
4741
await fs.promises.writeFile('/input.js', DEFAULT_SNAP_BUNDLE);
4842

4943
jest.spyOn(console, 'log').mockImplementation();
@@ -79,7 +73,7 @@ describe('buildHandler', () => {
7973
expect(process.exitCode).not.toBe(1);
8074
expect(build).toHaveBeenCalledWith(config, {
8175
analyze: true,
82-
evaluate: false,
76+
evaluate: true,
8377
spinner: expect.any(Object),
8478
});
8579

@@ -106,8 +100,11 @@ describe('buildHandler', () => {
106100
await buildHandler(config);
107101

108102
expect(process.exitCode).not.toBe(1);
109-
expect(build).toHaveBeenCalled();
110-
expect(evaluate).not.toHaveBeenCalled();
103+
expect(build).toHaveBeenCalledWith(config, {
104+
analyze: false,
105+
evaluate: false,
106+
spinner: expect.any(Object),
107+
});
111108
});
112109

113110
it('checks if the input file exists', async () => {
@@ -121,7 +118,7 @@ describe('buildHandler', () => {
121118
expect(process.exitCode).toBe(1);
122119
expect(log).toHaveBeenCalledWith(
123120
expect.stringMatching(
124-
/Input file not found: ".+"\. Make sure that the "input" field in your snap config is correct\./u,
121+
/Input file not found: ".+"\. Make sure that the "input" field in your Snap config is correct\./u,
125122
),
126123
);
127124
});

packages/snaps-cli/src/commands/build/build.ts

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,57 @@
11
import { isFile } from '@metamask/snaps-utils/node';
22
import { assert } from '@metamask/utils';
3-
import { resolve as pathResolve } from 'path';
43

54
import { build } from './implementation';
65
import { getBundleAnalyzerPort } from './utils';
76
import type { ProcessedConfig } from '../../config';
87
import { CommandError } from '../../errors';
98
import type { Steps } from '../../utils';
10-
import { success, executeSteps, info } from '../../utils';
11-
import { evaluate } from '../eval';
9+
import { success, executeSteps } from '../../utils';
1210

1311
export type BuildContext = {
1412
analyze: boolean;
1513
build: boolean;
1614
config: ProcessedConfig;
15+
exports?: string[];
1716
port?: number;
1817
};
1918

2019
export const steps: Steps<BuildContext> = [
2120
{
2221
name: 'Checking the input file.',
23-
condition: ({ build }) => build,
22+
condition: ({ build: enableBuild }) => enableBuild,
2423
task: async ({ config }) => {
2524
const { input } = config;
2625

2726
if (!(await isFile(input))) {
2827
throw new CommandError(
29-
`Input file not found: "${input}". Make sure that the "input" field in your snap config is correct.`,
28+
`Input file not found: "${input}". Make sure that the "input" field in your Snap config is correct.`,
3029
);
3130
}
3231
},
3332
},
3433
{
35-
name: 'Building the snap bundle.',
36-
condition: ({ build }) => build,
37-
task: async ({ analyze, build: enableBuild, config, spinner }) => {
38-
// We don't evaluate the bundle here, because it's done in a separate
39-
// step.
34+
name: 'Building the Snap bundle.',
35+
condition: ({ build: enableBuild }) => enableBuild,
36+
task: async (context) => {
37+
const { analyze, config, spinner } = context;
38+
4039
const compiler = await build(config, {
4140
analyze,
42-
evaluate: false,
41+
evaluate: config.evaluate,
4342
spinner,
4443
});
4544

4645
if (analyze) {
4746
return {
48-
analyze,
49-
build: enableBuild,
50-
config,
51-
spinner,
47+
...context,
5248
port: await getBundleAnalyzerPort(compiler),
5349
};
5450
}
5551

5652
return undefined;
5753
},
5854
},
59-
{
60-
name: 'Evaluating the snap bundle.',
61-
condition: ({ build, config }) => build && config.evaluate,
62-
task: async ({ config, spinner }) => {
63-
const path = pathResolve(
64-
process.cwd(),
65-
config.output.path,
66-
config.output.filename,
67-
);
68-
69-
await evaluate(path);
70-
71-
info(`Snap bundle evaluated successfully.`, spinner);
72-
},
73-
},
7455
{
7556
name: 'Running analyser.',
7657
condition: ({ analyze }) => analyze,

packages/snaps-cli/src/commands/build/implementation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '@metamask/snaps-utils/test-utils';
99
import type { SemVerVersion } from '@metamask/utils';
1010
import normalFs from 'fs';
11-
import { dirname, resolve } from 'path';
11+
import { dirname } from 'path';
1212
import type { Configuration } from 'webpack';
1313

1414
import { build } from './implementation';

packages/snaps-cli/src/commands/build/implementation.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ import { getCompiler } from '../../webpack';
1111
* @param options - The Webpack options.
1212
* @returns A promise that resolves when the bundle is built.
1313
*/
14-
export async function build(
15-
config: ProcessedConfig,
16-
options?: WebpackOptions,
17-
) {
14+
export async function build(config: ProcessedConfig, options?: WebpackOptions) {
1815
const compiler = await getCompiler(config, options);
1916
return await new Promise<Compiler>((resolve, reject) => {
2017
compiler.run((runError) => {

0 commit comments

Comments
 (0)