@openzeppelin/upgrades-core 1.46.0 → 1.47.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 (63) hide show
  1. package/dist/cli/validate/validate-upgrade-safety.test.js +18 -0
  2. package/dist/cli/validate/validate-upgrade-safety.test.js.map +1 -1
  3. package/dist/storage/compare.d.ts.map +1 -1
  4. package/dist/storage/compare.js +3 -3
  5. package/dist/storage/compare.js.map +1 -1
  6. package/dist/storage/extract.d.ts +15 -3
  7. package/dist/storage/extract.d.ts.map +1 -1
  8. package/dist/storage/extract.js +47 -15
  9. package/dist/storage/extract.js.map +1 -1
  10. package/dist/storage/namespace.d.ts.map +1 -1
  11. package/dist/storage/namespace.js +8 -2
  12. package/dist/storage/namespace.js.map +1 -1
  13. package/dist/storage/report-rename-retype.test.js +30 -0
  14. package/dist/storage/report-rename-retype.test.js.map +1 -1
  15. package/dist/storage/report.js +9 -1
  16. package/dist/storage/report.js.map +1 -1
  17. package/dist/storage-namespaced-renamed-retyped.test.d.ts +2 -0
  18. package/dist/storage-namespaced-renamed-retyped.test.d.ts.map +1 -0
  19. package/dist/storage-namespaced-renamed-retyped.test.js +278 -0
  20. package/dist/storage-namespaced-renamed-retyped.test.js.map +1 -0
  21. package/dist/storage-struct-member-renamed-retyped.test.d.ts +2 -0
  22. package/dist/storage-struct-member-renamed-retyped.test.d.ts.map +1 -0
  23. package/dist/storage-struct-member-renamed-retyped.test.js +256 -0
  24. package/dist/storage-struct-member-renamed-retyped.test.js.map +1 -0
  25. package/dist/utils/annotations.d.ts +1 -0
  26. package/dist/utils/annotations.d.ts.map +1 -1
  27. package/dist/utils/annotations.js +4 -1
  28. package/dist/utils/annotations.js.map +1 -1
  29. package/dist/utils/struct-member-docs.d.ts +52 -0
  30. package/dist/utils/struct-member-docs.d.ts.map +1 -0
  31. package/dist/utils/struct-member-docs.js +264 -0
  32. package/dist/utils/struct-member-docs.js.map +1 -0
  33. package/dist/utils/struct-member-docs.test.d.ts +2 -0
  34. package/dist/utils/struct-member-docs.test.d.ts.map +1 -0
  35. package/dist/utils/struct-member-docs.test.js +320 -0
  36. package/dist/utils/struct-member-docs.test.js.map +1 -0
  37. package/dist/validate/data.d.ts +1 -1
  38. package/dist/validate/data.d.ts.map +1 -1
  39. package/dist/validate/data.js +1 -1
  40. package/dist/validate/run.d.ts +1 -1
  41. package/dist/validate/run.d.ts.map +1 -1
  42. package/dist/validate/run.js +22 -2
  43. package/dist/validate/run.js.map +1 -1
  44. package/package.json +3 -3
  45. package/src/cli/validate/validate-upgrade-safety.test.ts +35 -0
  46. package/src/cli/validate/validate-upgrade-safety.test.ts.md +10 -0
  47. package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
  48. package/src/storage/compare.ts +3 -4
  49. package/src/storage/extract.ts +55 -18
  50. package/src/storage/namespace.ts +10 -3
  51. package/src/storage/report-rename-retype.test.ts +41 -0
  52. package/src/storage/report.ts +9 -1
  53. package/src/storage-namespaced-renamed-retyped.test.ts +338 -0
  54. package/src/storage-namespaced-renamed-retyped.test.ts.md +183 -0
  55. package/src/storage-namespaced-renamed-retyped.test.ts.snap +0 -0
  56. package/src/storage-struct-member-renamed-retyped.test.ts +299 -0
  57. package/src/storage-struct-member-renamed-retyped.test.ts.md +260 -0
  58. package/src/storage-struct-member-renamed-retyped.test.ts.snap +0 -0
  59. package/src/utils/annotations.ts +4 -4
  60. package/src/utils/struct-member-docs.test.ts +351 -0
  61. package/src/utils/struct-member-docs.ts +293 -0
  62. package/src/validate/data.ts +2 -2
  63. package/src/validate/run.ts +23 -1
@@ -0,0 +1,260 @@
1
+ # Snapshot report for `src/storage-struct-member-renamed-retyped.test.ts`
2
+
3
+ The actual snapshot is saved in `storage-struct-member-renamed-retyped.test.ts.snap`.
4
+
5
+ Generated by [AVA](https://avajs.dev).
6
+
7
+ ## layout
8
+
9
+ > Snapshot 1
10
+
11
+ {
12
+ baseSlot: undefined,
13
+ namespaces: [],
14
+ storage: [
15
+ {
16
+ contract: 'StructMemberRenamedRetypedV2',
17
+ label: 's',
18
+ offset: 0,
19
+ renamedFrom: undefined,
20
+ retypedFrom: undefined,
21
+ slot: '0',
22
+ src: 'contracts/test/StructMemberRenamedRetyped.sol:62',
23
+ type: 't_struct(S)storage',
24
+ },
25
+ {
26
+ contract: 'StructMemberRenamedRetypedV2',
27
+ label: 'm',
28
+ offset: 0,
29
+ renamedFrom: undefined,
30
+ retypedFrom: undefined,
31
+ slot: '5',
32
+ src: 'contracts/test/StructMemberRenamedRetyped.sol:63',
33
+ type: 't_mapping(t_uint256,t_struct(S)storage)',
34
+ },
35
+ {
36
+ contract: 'StructMemberRenamedRetypedV2',
37
+ label: 'arr',
38
+ offset: 0,
39
+ renamedFrom: undefined,
40
+ retypedFrom: undefined,
41
+ slot: '6',
42
+ src: 'contracts/test/StructMemberRenamedRetyped.sol:64',
43
+ type: 't_array(t_struct(S)storage)dyn_storage',
44
+ },
45
+ {
46
+ contract: 'StructMemberRenamedRetypedV2',
47
+ label: 'outer',
48
+ offset: 0,
49
+ renamedFrom: undefined,
50
+ retypedFrom: undefined,
51
+ slot: '7',
52
+ src: 'contracts/test/StructMemberRenamedRetyped.sol:65',
53
+ type: 't_struct(Outer)storage',
54
+ },
55
+ {
56
+ contract: 'StructMemberRenamedRetypedV2',
57
+ label: 'other',
58
+ offset: 0,
59
+ renamedFrom: undefined,
60
+ retypedFrom: undefined,
61
+ slot: '10',
62
+ src: 'contracts/test/StructMemberRenamedRetyped.sol:66',
63
+ type: 't_struct(OtherFileV2)storage',
64
+ },
65
+ ],
66
+ types: [
67
+ [
68
+ 't_array(t_struct(S)storage)dyn_storage',
69
+ {
70
+ label: 'struct StructMemberRenamedRetypedV2.S[]',
71
+ members: undefined,
72
+ numberOfBytes: '32',
73
+ },
74
+ ],
75
+ [
76
+ 't_bool',
77
+ {
78
+ label: 'bool',
79
+ members: undefined,
80
+ numberOfBytes: '1',
81
+ },
82
+ ],
83
+ [
84
+ 't_bytes32',
85
+ {
86
+ label: 'bytes32',
87
+ members: undefined,
88
+ numberOfBytes: '32',
89
+ },
90
+ ],
91
+ [
92
+ 't_mapping(t_uint256,t_struct(S)storage)',
93
+ {
94
+ label: 'mapping(uint256 => struct StructMemberRenamedRetypedV2.S)',
95
+ members: undefined,
96
+ numberOfBytes: '32',
97
+ },
98
+ ],
99
+ [
100
+ 't_struct(Inner)storage',
101
+ {
102
+ label: 'struct StructMemberRenamedRetypedV2.Inner',
103
+ members: [
104
+ {
105
+ label: 'g',
106
+ offset: 0,
107
+ renamedFrom: 'a',
108
+ retypedFrom: 'uint8',
109
+ slot: '0',
110
+ type: 't_bool',
111
+ },
112
+ {
113
+ label: 'bb',
114
+ offset: 0,
115
+ renamedFrom: 'b',
116
+ slot: '1',
117
+ type: 't_bytes32',
118
+ },
119
+ ],
120
+ numberOfBytes: '64',
121
+ },
122
+ ],
123
+ [
124
+ 't_struct(OtherFileV2)storage',
125
+ {
126
+ label: 'struct OtherFileV2',
127
+ members: [
128
+ {
129
+ label: 'a',
130
+ offset: 0,
131
+ retypedFrom: 'bytes32',
132
+ slot: '0',
133
+ type: 't_struct(TBytes32)storage',
134
+ },
135
+ {
136
+ label: 'c',
137
+ offset: 0,
138
+ renamedFrom: 'b',
139
+ slot: '1',
140
+ type: 't_uint256',
141
+ },
142
+ ],
143
+ numberOfBytes: '64',
144
+ },
145
+ ],
146
+ [
147
+ 't_struct(Outer)storage',
148
+ {
149
+ label: 'struct StructMemberRenamedRetypedV2.Outer',
150
+ members: [
151
+ {
152
+ label: 'inner',
153
+ offset: 0,
154
+ slot: '0',
155
+ type: 't_struct(Inner)storage',
156
+ },
157
+ {
158
+ label: 'n',
159
+ offset: 0,
160
+ slot: '2',
161
+ type: 't_uint256',
162
+ },
163
+ ],
164
+ numberOfBytes: '96',
165
+ },
166
+ ],
167
+ [
168
+ 't_struct(S)storage',
169
+ {
170
+ label: 'struct StructMemberRenamedRetypedV2.S',
171
+ members: [
172
+ {
173
+ label: 'x',
174
+ offset: 0,
175
+ retypedFrom: 'bytes32',
176
+ slot: '0',
177
+ type: 't_struct(TBytes32)storage',
178
+ },
179
+ {
180
+ label: 'z',
181
+ offset: 0,
182
+ renamedFrom: 'y',
183
+ slot: '1',
184
+ type: 't_uint256',
185
+ },
186
+ {
187
+ label: 'f',
188
+ offset: 0,
189
+ retypedFrom: 'uint8',
190
+ slot: '2',
191
+ type: 't_bool',
192
+ },
193
+ {
194
+ label: 'c',
195
+ offset: 0,
196
+ slot: '3',
197
+ type: 't_uint256',
198
+ },
199
+ {
200
+ label: 'd',
201
+ offset: 0,
202
+ renamedFrom: 'oldD',
203
+ slot: '4',
204
+ type: 't_uint256',
205
+ },
206
+ ],
207
+ numberOfBytes: '160',
208
+ },
209
+ ],
210
+ [
211
+ 't_struct(TBytes32)storage',
212
+ {
213
+ label: 'struct TransientSlot.TBytes32',
214
+ members: [
215
+ {
216
+ label: '_placeholder',
217
+ offset: 0,
218
+ slot: '0',
219
+ type: 't_bytes32',
220
+ },
221
+ ],
222
+ numberOfBytes: '32',
223
+ },
224
+ ],
225
+ [
226
+ 't_uint256',
227
+ {
228
+ label: 'uint256',
229
+ members: undefined,
230
+ numberOfBytes: '32',
231
+ },
232
+ ],
233
+ ],
234
+ }
235
+
236
+ ## annotated members without layout info
237
+
238
+ > Snapshot 1
239
+
240
+ `contracts/test/StructMemberRenamedRetyped.sol:62: Upgraded \`s\` to an incompatible type␊
241
+ - Bad upgrade from struct StructMemberRenamedRetypedV1.S to struct StructMemberRenamedRetypedV2.S␊
242
+ - In struct StructMemberRenamedRetypedV2.S␊
243
+ - Layout could have changed for \`x\` (bytes32 -> struct TransientSlot.TBytes32)␊
244
+ ␊
245
+ contracts/test/StructMemberRenamedRetyped.sol:63: Upgraded \`m\` to an incompatible type␊
246
+ - In mapping(uint256 => struct StructMemberRenamedRetypedV2.S)␊
247
+ - Bad upgrade from struct StructMemberRenamedRetypedV1.S to struct StructMemberRenamedRetypedV2.S␊
248
+ - In struct StructMemberRenamedRetypedV2.S␊
249
+ - Layout could have changed for \`x\` (bytes32 -> struct TransientSlot.TBytes32)␊
250
+ ␊
251
+ contracts/test/StructMemberRenamedRetyped.sol:64: Upgraded \`arr\` to an incompatible type␊
252
+ - In struct StructMemberRenamedRetypedV2.S[]␊
253
+ - Bad upgrade from struct StructMemberRenamedRetypedV1.S to struct StructMemberRenamedRetypedV2.S␊
254
+ - In struct StructMemberRenamedRetypedV2.S␊
255
+ - Layout could have changed for \`x\` (bytes32 -> struct TransientSlot.TBytes32)␊
256
+ ␊
257
+ contracts/test/StructMemberRenamedRetyped.sol:66: Upgraded \`other\` to an incompatible type␊
258
+ - Bad upgrade from struct OtherFileV1 to struct OtherFileV2␊
259
+ - In struct OtherFileV2␊
260
+ - Layout could have changed for \`a\` (bytes32 -> struct TransientSlot.TBytes32)`
@@ -1,6 +1,9 @@
1
1
  import { execall } from './execall';
2
2
  import { Node } from 'solidity-ast/node';
3
3
 
4
+ // Matches one NatSpec tag: `@title:tag args`, where the args run until the next line that starts with a tag
5
+ export const ANNOTATION_REGEX = /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s*@\w+)[^])*)/m;
6
+
4
7
  /**
5
8
  * Whether the given doc string has an annotation tag.
6
9
  */
@@ -18,10 +21,7 @@ export function hasAnnotationTag(doc: string, tag: string): boolean {
18
21
  */
19
22
  export function getAnnotationArgs(doc: string, tag: string, supportedArgs?: readonly string[]) {
20
23
  const result: string[] = [];
21
- for (const { groups } of execall(
22
- /^\s*(?:@(?<title>\w+)(?::(?<tag>[a-z][a-z-]*))? )?(?<args>(?:(?!^\s*@\w+)[^])*)/m,
23
- doc,
24
- )) {
24
+ for (const { groups } of execall(ANNOTATION_REGEX, doc)) {
25
25
  if (groups && groups.title === 'custom' && groups.tag === tag) {
26
26
  const trimmedArgs = groups.args.trim();
27
27
  if (trimmedArgs.length > 0) {
@@ -0,0 +1,351 @@
1
+ import test, { ExecutionContext } from 'ava';
2
+ import { artifacts, run } from 'hardhat';
3
+ import {
4
+ TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD,
5
+ TASK_COMPILE_SOLIDITY_RUN_SOLC,
6
+ TASK_COMPILE_SOLIDITY_RUN_SOLCJS,
7
+ } from 'hardhat/builtin-tasks/task-names';
8
+ import { SolcBuild } from 'hardhat/types/builtin-tasks';
9
+ import { promises as fs } from 'fs';
10
+ import { ContractDefinition, StructDefinition, VariableDeclaration } from 'solidity-ast';
11
+ import { findAll } from 'solidity-ast/utils';
12
+ import * as versions from 'compare-versions';
13
+
14
+ import { SolcInput, SolcOutput } from '../solc-api';
15
+ import { solcInputOutputDecoder } from '../src-decoder';
16
+ import { getRetypedRenamedFromDoc } from '../storage/extract';
17
+ import {
18
+ getDocumentationBefore,
19
+ getStructMemberDocumentation,
20
+ makeStructDocsResolver,
21
+ UnexpectedStructSourceError,
22
+ } from './struct-member-docs';
23
+
24
+ const twinsSource = 'contracts/test/StructMemberDocs.sol';
25
+
26
+ function getPositions(node: { src: string }) {
27
+ const [start, length] = node.src.split(':').map(Number);
28
+ return { start, end: start + length };
29
+ }
30
+
31
+ async function getBuildInfo(source: string, contract: string) {
32
+ const buildInfo = await artifacts.getBuildInfo(`${source}:${contract}`);
33
+ if (buildInfo === undefined) {
34
+ throw new Error('Build info not found');
35
+ }
36
+ return buildInfo;
37
+ }
38
+
39
+ function getSource(input: SolcInput, source: string): Buffer {
40
+ const content = input.sources[source].content;
41
+ if (content === undefined) {
42
+ throw new Error(`Content for ${source} not available`);
43
+ }
44
+ return Buffer.from(content, 'utf8');
45
+ }
46
+
47
+ function getContract(output: SolcOutput, source: string, name: string): ContractDefinition {
48
+ const contractDef = [...findAll('ContractDefinition', output.sources[source].ast)].find(c => c.name === name);
49
+ if (contractDef === undefined) {
50
+ throw new Error(`Contract ${name} not found`);
51
+ }
52
+ return contractDef;
53
+ }
54
+
55
+ function getStruct(contractDef: ContractDefinition, name: string): StructDefinition {
56
+ const structDef = [...findAll('StructDefinition', contractDef)].find(s => s.name === name);
57
+ if (structDef === undefined) {
58
+ throw new Error(`Struct ${name} not found`);
59
+ }
60
+ return structDef;
61
+ }
62
+
63
+ /**
64
+ * Asserts that each member of the struct in the twin fixture has documentation exactly when the state variable with
65
+ * the same name has, with the same annotations.
66
+ */
67
+ function assertTwins(t: ExecutionContext, input: SolcInput, output: SolcOutput) {
68
+ const contractDef = getContract(output, twinsSource, 'StructMemberDocs');
69
+ const stateVars: VariableDeclaration[] = [];
70
+ for (const node of contractDef.nodes) {
71
+ if (node.nodeType === 'VariableDeclaration') {
72
+ stateVars.push(node);
73
+ }
74
+ }
75
+ const structDef = getStruct(contractDef, 'S');
76
+ const names = structDef.members.map(m => m.name);
77
+ t.deepEqual(
78
+ stateVars.map(v => v.name),
79
+ names,
80
+ );
81
+
82
+ const expected = new Map<string, string>();
83
+ for (const stateVar of stateVars) {
84
+ if (stateVar.documentation) {
85
+ expected.set(stateVar.name, stateVar.documentation.text);
86
+ }
87
+ }
88
+ t.true(expected.size >= 25);
89
+
90
+ const docs = getStructMemberDocumentation(structDef, getSource(input, twinsSource));
91
+ t.deepEqual([...docs.keys()], [...expected.keys()]);
92
+ for (const name of names) {
93
+ t.deepEqual(
94
+ getRetypedRenamedFromDoc(docs.get(name) ?? ''),
95
+ getRetypedRenamedFromDoc(expected.get(name) ?? ''),
96
+ name,
97
+ );
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Compiles the twin fixture again with the given line endings.
103
+ */
104
+ async function compileTwins(lineEnding: string) {
105
+ const { input, solcVersion } = await getBuildInfo(twinsSource, 'StructMemberDocs');
106
+ const content = getSource(input, twinsSource)
107
+ .toString('utf8')
108
+ .replace(/\r\n|\r|\n/g, lineEnding);
109
+ const newInput = { ...input, sources: { [twinsSource]: { content } } };
110
+ return { input: newInput, output: await hardhatCompile(newInput, solcVersion) };
111
+ }
112
+
113
+ async function hardhatCompile(input: SolcInput, solcVersion: string): Promise<SolcOutput> {
114
+ const solcBuild: SolcBuild = await run(TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD, {
115
+ quiet: true,
116
+ solcVersion,
117
+ });
118
+
119
+ const output: SolcOutput = solcBuild.isSolcJs
120
+ ? await run(TASK_COMPILE_SOLIDITY_RUN_SOLCJS, { input, solcJsPath: solcBuild.compilerPath })
121
+ : await run(TASK_COMPILE_SOLIDITY_RUN_SOLC, { input, solcPath: solcBuild.compilerPath });
122
+
123
+ const errors = output.errors?.filter(e => e.severity === 'error');
124
+ if (errors !== undefined && errors.length > 0) {
125
+ throw new Error(errors.map(e => e.formattedMessage).join('\n'));
126
+ }
127
+ return output;
128
+ }
129
+
130
+ test('member documentation has the same annotations as state variable documentation', async t => {
131
+ const { input, output } = await getBuildInfo(twinsSource, 'StructMemberDocs');
132
+ // The fixture has a line of only whitespace between two doc lines, and a doc block line of a star and a space
133
+ const content = getSource(input, twinsSource).toString('utf8');
134
+ t.regex(content, /\n[ \t]+\r?\n/);
135
+ t.regex(content, /\n[ \t]+\* \r?\n/);
136
+ assertTwins(t, input, output);
137
+ });
138
+
139
+ test('member documentation with CRLF line endings', async t => {
140
+ const { input, output } = await compileTwins('\r\n');
141
+ assertTwins(t, input, output);
142
+ });
143
+
144
+ test('member documentation with CR line endings', async t => {
145
+ const { input, output } = await compileTwins('\r');
146
+ assertTwins(t, input, output);
147
+ });
148
+
149
+ test('member documentation', async t => {
150
+ const source = 'contracts/test/StructMemberRenamedRetyped.sol';
151
+ const { input, output } = await getBuildInfo(source, 'StructMemberRenamedRetypedV2');
152
+ const contractDef = getContract(output, source, 'StructMemberRenamedRetypedV2');
153
+ const content = getSource(input, source);
154
+ t.deepEqual(
155
+ getStructMemberDocumentation(getStruct(contractDef, 'S'), content),
156
+ new Map([
157
+ ['x', '@custom:oz-retyped-from bytes32'],
158
+ ['z', ' @dev documented in a block\n @custom:oz-renamed-from y'],
159
+ ['f', '@custom:oz-retyped-from uint8'],
160
+ ['d', '@custom:oz-renamed-from oldD'],
161
+ ]),
162
+ );
163
+ t.deepEqual(
164
+ getStructMemberDocumentation(getStruct(contractDef, 'Inner'), content),
165
+ new Map([
166
+ ['g', '@custom:oz-renamed-from a\n@custom:oz-retyped-from uint8 '],
167
+ ['bb', '@custom:oz-renamed-from b\n @dev documented on a second line'],
168
+ ]),
169
+ );
170
+ t.deepEqual(getStructMemberDocumentation(getStruct(contractDef, 'Outer'), content), new Map());
171
+ });
172
+
173
+ test('struct shapes', async t => {
174
+ const { input, output } = await getBuildInfo(twinsSource, 'StructMemberDocs');
175
+ const contractDef = getContract(output, twinsSource, 'StructMemberDocs');
176
+ const source = getSource(input, twinsSource);
177
+ const docs = (name: string) => Object.fromEntries(getStructMemberDocumentation(getStruct(contractDef, name), source));
178
+ t.deepEqual(docs('HeaderComments'), {});
179
+ t.deepEqual(docs('BraceOnNextLine'), { first: '@custom:oz-renamed-from a' });
180
+ t.deepEqual(docs('OneLine'), {});
181
+ t.deepEqual(docs('DanglingDoc'), {});
182
+ });
183
+
184
+ /**
185
+ * Yields the build info of every compilation of the fixtures.
186
+ */
187
+ async function* getBuildInfos(): AsyncGenerator<{ solcVersion: string; input: SolcInput; output: SolcOutput }> {
188
+ for (const buildInfoPath of await artifacts.getBuildInfoPaths()) {
189
+ yield JSON.parse(await fs.readFile(buildInfoPath, 'utf8'));
190
+ }
191
+ }
192
+
193
+ test('parity with solc documentation of state variables', async t => {
194
+ // The annotations read from the source must be those that solc reads from the same comments, for every solc version
195
+ // used by the fixtures. State variables are compared since solc includes their documentation in the AST. The texts
196
+ // are compared as well, except for whitespace at their ends, which is where the text read from the source can differ.
197
+ let compared = 0;
198
+ for await (const buildInfo of getBuildInfos()) {
199
+ // Solidity 0.5 does not include documentation in the AST
200
+ const hasDocumentation = versions.compare(buildInfo.solcVersion, '0.6.0', '>=');
201
+ for (const [sourcePath, { ast }] of Object.entries(buildInfo.output.sources)) {
202
+ const source = getSource(buildInfo.input, sourcePath);
203
+ for (const contractDef of findAll('ContractDefinition', ast)) {
204
+ // The first declaration is preceded by the opening brace of the contract
205
+ let prevEnd = source.indexOf('{', getPositions(contractDef).start) + 1;
206
+ for (const node of contractDef.nodes) {
207
+ const { start, end } = getPositions(node);
208
+ if (node.nodeType === 'VariableDeclaration') {
209
+ const description = `${buildInfo.solcVersion} ${sourcePath}:${contractDef.name}.${node.name}`;
210
+ let actual;
211
+ try {
212
+ actual = getDocumentationBefore(source, prevEnd, start);
213
+ } catch (e) {
214
+ t.fail(`${description}: ${e}`);
215
+ }
216
+ if (hasDocumentation) {
217
+ const expected = node.documentation?.text;
218
+ t.is(actual?.trim(), expected?.trim(), description);
219
+ t.deepEqual(
220
+ getRetypedRenamedFromDoc(actual ?? ''),
221
+ getRetypedRenamedFromDoc(expected ?? ''),
222
+ description,
223
+ );
224
+ if (expected !== undefined) {
225
+ compared += 1;
226
+ }
227
+ }
228
+ }
229
+ prevEnd = skipSemicolon(source, end);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ t.true(compared >= 50, `Compared ${compared} documented state variables`);
235
+ });
236
+
237
+ /**
238
+ * The source range of some declarations, such as state variables, does not include the `;` that ends them, nor the
239
+ * whitespace and comments before it.
240
+ */
241
+ function skipSemicolon(source: Buffer, pos: number): number {
242
+ const match = /^(?:[ \t\r\n]|\/\/[^\r\n]*|\/\*[^]*?\*\/)*;/.exec(source.toString('latin1', pos));
243
+ return match === null ? pos : pos + match[0].length;
244
+ }
245
+
246
+ test('struct definitions of every fixture', async t => {
247
+ // The source of every struct must have the shape that the scanner expects, whichever solc version compiled it, and
248
+ // a struct without doc comments has no member documentation
249
+ let checked = 0;
250
+ for await (const buildInfo of getBuildInfos()) {
251
+ for (const [sourcePath, { ast }] of Object.entries(buildInfo.output.sources)) {
252
+ const source = getSource(buildInfo.input, sourcePath);
253
+ for (const structDef of findAll('StructDefinition', ast)) {
254
+ const description = `${buildInfo.solcVersion} ${sourcePath}:${structDef.canonicalName}`;
255
+ let docs;
256
+ try {
257
+ docs = getStructMemberDocumentation(structDef, source);
258
+ } catch (e) {
259
+ t.fail(`${description}: ${e}`);
260
+ continue;
261
+ }
262
+ const { start, end } = getPositions(structDef);
263
+ const text = source.toString('utf8', start, end);
264
+ if (!text.includes('///') && !text.includes('/**')) {
265
+ t.is(docs.size, 0, description);
266
+ }
267
+ checked += 1;
268
+ }
269
+ }
270
+ }
271
+ t.true(checked >= 100, `Checked ${checked} structs`);
272
+ });
273
+
274
+ test('struct not in the original compilation', async t => {
275
+ const source = 'contracts/test/StructMemberRenamedRetyped.sol';
276
+ const buildInfo = await getBuildInfo(source, 'StructMemberRenamedRetypedV2');
277
+ const decodeSrc = solcInputOutputDecoder(buildInfo.input, buildInfo.output);
278
+ const resolver = makeStructDocsResolver(buildInfo.input, buildInfo.output, decodeSrc);
279
+ const [structDef] = findAll('StructDefinition', buildInfo.output.sources[source].ast);
280
+ t.true(resolver(structDef) instanceof Map);
281
+ t.throws(() => resolver({ ...structDef, canonicalName: 'Unknown.S' }), { message: /Broken invariant/ });
282
+ });
283
+
284
+ test('unexpected text', t => {
285
+ const source = Buffer.from('/* a */ /// b\n uint8 x /// c\n', 'utf8');
286
+ t.is(getDocumentationBefore(source, 0, source.indexOf('uint8')), 'b');
287
+
288
+ const error = t.throws(() => getDocumentationBefore(source, 0, source.length), {
289
+ instanceOf: UnexpectedStructSourceError,
290
+ });
291
+ t.is(error?.position, source.indexOf('uint8'));
292
+
293
+ // Only spaces, tabs and line breaks are whitespace
294
+ for (const char of ['\u00a0', '\f']) {
295
+ const text = Buffer.from(`/// a\n${char}uint8 x`, 'utf8');
296
+ const error = t.throws(() => getDocumentationBefore(text, 0, text.indexOf('uint8')), {
297
+ instanceOf: UnexpectedStructSourceError,
298
+ });
299
+ t.is(error?.position, text.indexOf(char));
300
+ }
301
+
302
+ t.throws(() => getDocumentationBefore(Buffer.from('/** a\n'), 0, 6), { instanceOf: UnexpectedStructSourceError });
303
+ t.throws(() => getDocumentationBefore(Buffer.from('/* a\n'), 0, 5), { instanceOf: UnexpectedStructSourceError });
304
+ });
305
+
306
+ /**
307
+ * Builds the AST of a struct from its source text, in which each member declaration is enclosed in brackets that are
308
+ * not part of the source. The source range of the struct is the whole text.
309
+ */
310
+ function makeStruct(name: string, text: string): { structDef: StructDefinition; source: Buffer } {
311
+ const members = [];
312
+ let source = '';
313
+ for (const [i, part] of text.split(/[[\]]/).entries()) {
314
+ // The parts at odd indexes are the member declarations
315
+ if (i % 2 === 1) {
316
+ const src = `${Buffer.byteLength(source)}:${Buffer.byteLength(part)}:0`;
317
+ members.push({ nodeType: 'VariableDeclaration', name: part.split(/\s+/).pop(), src });
318
+ }
319
+ source += part;
320
+ }
321
+ const structDef = { nodeType: 'StructDefinition', name, src: `0:${Buffer.byteLength(source)}:0`, members };
322
+ return { structDef: structDef as unknown as StructDefinition, source: Buffer.from(source, 'utf8') };
323
+ }
324
+
325
+ function assertUnexpectedStruct(t: ExecutionContext, name: string, text: string, position: (source: Buffer) => number) {
326
+ const { structDef, source } = makeStruct(name, text);
327
+ const error = t.throws(() => getStructMemberDocumentation(structDef, source), {
328
+ instanceOf: UnexpectedStructSourceError,
329
+ });
330
+ t.is(error?.position, position(source), text);
331
+ }
332
+
333
+ test('unexpected struct source', t => {
334
+ const { structDef, source } = makeStruct('S', 'struct S {\n /// a\n [uint256 x];\n [uint256 y] /// b\n ; /// c\n}');
335
+ t.deepEqual(getStructMemberDocumentation(structDef, source), new Map([['x', 'a']]));
336
+
337
+ assertUnexpectedStruct(t, 'S', 'contract S { [uint256 x]; }', () => 0);
338
+ assertUnexpectedStruct(t, 'T', 'struct S { [uint256 x]; }', s => s.indexOf('S'));
339
+ assertUnexpectedStruct(t, 'S', 'struct S [uint256 x]; }', s => s.indexOf('uint256'));
340
+ assertUnexpectedStruct(t, 'S', 'struct S { [uint256 x] [uint256 y]; }', s => s.indexOf('uint256 y'));
341
+ assertUnexpectedStruct(t, 'S', 'struct S { [uint256 x]; } ', s => s.indexOf('}') + 1);
342
+ assertUnexpectedStruct(t, 'S', 'struct S { [uint256 x]; \u00a0}', s => s.indexOf('\u00a0'));
343
+ assertUnexpectedStruct(t, 'S', 'struct S { /** a\n [uint256 x]; }', s => s.indexOf('/**'));
344
+
345
+ // A member that starts before the end of the previous member
346
+ structDef.members.reverse();
347
+ const error = t.throws(() => getStructMemberDocumentation(structDef, source), {
348
+ instanceOf: UnexpectedStructSourceError,
349
+ });
350
+ t.is(error?.position, source.indexOf('uint256 x'));
351
+ });