@openzeppelin/upgrades-core 1.45.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.
- package/dist/cli/validate/validate-upgrade-safety.test.js +18 -0
- package/dist/cli/validate/validate-upgrade-safety.test.js.map +1 -1
- package/dist/storage/compare.d.ts.map +1 -1
- package/dist/storage/compare.js +3 -3
- package/dist/storage/compare.js.map +1 -1
- package/dist/storage/extract.d.ts +15 -3
- package/dist/storage/extract.d.ts.map +1 -1
- package/dist/storage/extract.js +68 -22
- package/dist/storage/extract.js.map +1 -1
- package/dist/storage/namespace.d.ts.map +1 -1
- package/dist/storage/namespace.js +26 -13
- package/dist/storage/namespace.js.map +1 -1
- package/dist/storage/report-rename-retype.test.js +30 -0
- package/dist/storage/report-rename-retype.test.js.map +1 -1
- package/dist/storage/report.js +9 -1
- package/dist/storage/report.js.map +1 -1
- package/dist/storage-erc7201-builtin.test.d.ts +2 -0
- package/dist/storage-erc7201-builtin.test.d.ts.map +1 -0
- package/dist/storage-erc7201-builtin.test.js +105 -0
- package/dist/storage-erc7201-builtin.test.js.map +1 -0
- package/dist/storage-namespaced-renamed-retyped.test.d.ts +2 -0
- package/dist/storage-namespaced-renamed-retyped.test.d.ts.map +1 -0
- package/dist/storage-namespaced-renamed-retyped.test.js +278 -0
- package/dist/storage-namespaced-renamed-retyped.test.js.map +1 -0
- package/dist/storage-struct-member-renamed-retyped.test.d.ts +2 -0
- package/dist/storage-struct-member-renamed-retyped.test.d.ts.map +1 -0
- package/dist/storage-struct-member-renamed-retyped.test.js +256 -0
- package/dist/storage-struct-member-renamed-retyped.test.js.map +1 -0
- package/dist/utils/annotations.d.ts +1 -0
- package/dist/utils/annotations.d.ts.map +1 -1
- package/dist/utils/annotations.js +4 -1
- package/dist/utils/annotations.js.map +1 -1
- package/dist/utils/erc7201-builtin.d.ts +24 -0
- package/dist/utils/erc7201-builtin.d.ts.map +1 -0
- package/dist/utils/erc7201-builtin.js +83 -0
- package/dist/utils/erc7201-builtin.js.map +1 -0
- package/dist/utils/erc7201.d.ts.map +1 -1
- package/dist/utils/erc7201.js +5 -3
- package/dist/utils/erc7201.js.map +1 -1
- package/dist/utils/erc7201.test.js +10 -0
- package/dist/utils/erc7201.test.js.map +1 -1
- package/dist/utils/integer-literals.d.ts +19 -6
- package/dist/utils/integer-literals.d.ts.map +1 -1
- package/dist/utils/integer-literals.js +28 -10
- package/dist/utils/integer-literals.js.map +1 -1
- package/dist/utils/integer-literals.test.js +12 -0
- package/dist/utils/integer-literals.test.js.map +1 -1
- package/dist/utils/struct-member-docs.d.ts +52 -0
- package/dist/utils/struct-member-docs.d.ts.map +1 -0
- package/dist/utils/struct-member-docs.js +264 -0
- package/dist/utils/struct-member-docs.js.map +1 -0
- package/dist/utils/struct-member-docs.test.d.ts +2 -0
- package/dist/utils/struct-member-docs.test.d.ts.map +1 -0
- package/dist/utils/struct-member-docs.test.js +320 -0
- package/dist/utils/struct-member-docs.test.js.map +1 -0
- package/dist/validate/data.d.ts +1 -1
- package/dist/validate/data.d.ts.map +1 -1
- package/dist/validate/data.js +1 -1
- package/dist/validate/run.d.ts +1 -1
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +22 -2
- package/dist/validate/run.js.map +1 -1
- package/package.json +5 -5
- package/src/cli/validate/validate-upgrade-safety.test.ts +35 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.md +10 -0
- package/src/cli/validate/validate-upgrade-safety.test.ts.snap +0 -0
- package/src/storage/compare.ts +3 -4
- package/src/storage/extract.ts +83 -30
- package/src/storage/namespace.ts +31 -14
- package/src/storage/report-rename-retype.test.ts +41 -0
- package/src/storage/report.ts +9 -1
- package/src/storage-erc7201-builtin.test.ts +127 -0
- package/src/storage-namespaced-renamed-retyped.test.ts +338 -0
- package/src/storage-namespaced-renamed-retyped.test.ts.md +183 -0
- package/src/storage-namespaced-renamed-retyped.test.ts.snap +0 -0
- package/src/storage-struct-member-renamed-retyped.test.ts +299 -0
- package/src/storage-struct-member-renamed-retyped.test.ts.md +260 -0
- package/src/storage-struct-member-renamed-retyped.test.ts.snap +0 -0
- package/src/utils/annotations.ts +4 -4
- package/src/utils/erc7201-builtin.ts +91 -0
- package/src/utils/erc7201.test.ts +12 -0
- package/src/utils/erc7201.ts +6 -4
- package/src/utils/integer-literals.test.ts +15 -1
- package/src/utils/integer-literals.ts +29 -10
- package/src/utils/struct-member-docs.test.ts +351 -0
- package/src/utils/struct-member-docs.ts +293 -0
- package/src/validate/data.ts +2 -2
- package/src/validate/run.ts +23 -1
package/src/utils/erc7201.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { keccak256 } from 'ethereumjs-util';
|
|
2
2
|
|
|
3
|
+
import { toBytes32Hex } from './integer-literals';
|
|
4
|
+
|
|
3
5
|
export const ERC7201_FORMULA_PREFIX = 'erc7201:';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -12,14 +14,14 @@ export const ERC7201_FORMULA_PREFIX = 'erc7201:';
|
|
|
12
14
|
export function calculateERC7201StorageLocation(id: string): string {
|
|
13
15
|
const firstHash = keccak256(Buffer.from(id));
|
|
14
16
|
const minusOne = BigInt(`0x${firstHash.toString('hex')}`) - 1n;
|
|
15
|
-
|
|
17
|
+
// Pad to 64 hex chars so that `Buffer.from` doesn't silently truncate odd-length input;
|
|
18
|
+
// `abi.encode(uint256)` is always 32 bytes.
|
|
19
|
+
const minusOneBuffer = Buffer.from(minusOne.toString(16).padStart(64, '0'), 'hex');
|
|
16
20
|
|
|
17
21
|
const secondHash = keccak256(minusOneBuffer);
|
|
18
22
|
|
|
19
23
|
const mask = BigInt('0xff');
|
|
20
24
|
const masked = BigInt(`0x${secondHash.toString('hex')}`) & ~mask;
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return `0x${padded}`;
|
|
26
|
+
return toBytes32Hex(masked);
|
|
25
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'ava';
|
|
2
2
|
|
|
3
|
-
import { normalizeUint256Literal } from './integer-literals';
|
|
3
|
+
import { normalizeUint256Literal, toBytes32Hex } from './integer-literals';
|
|
4
4
|
|
|
5
5
|
const ZEROES = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
|
6
6
|
|
|
@@ -71,3 +71,17 @@ test('full hex with leading and trailing zeroes', t => {
|
|
|
71
71
|
'0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00',
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
test('rejects values above uint256 max', t => {
|
|
76
|
+
const tooLarge = '0x10000000000000000000000000000000000000000000000000000000000000000';
|
|
77
|
+
const error = t.throws(() => normalizeUint256Literal(tooLarge));
|
|
78
|
+
t.regex(error?.message ?? '', /uint256 range/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('toBytes32Hex - boundaries', t => {
|
|
82
|
+
const MAX_HEX = '0x' + 'f'.repeat(64);
|
|
83
|
+
t.is(toBytes32Hex(0n), '0x' + '0'.repeat(64));
|
|
84
|
+
t.is(toBytes32Hex(BigInt(MAX_HEX)), MAX_HEX);
|
|
85
|
+
t.throws(() => toBytes32Hex(BigInt(MAX_HEX) + 1n), { message: /uint256 range/ });
|
|
86
|
+
t.throws(() => toBytes32Hex(-1n), { message: /uint256 range/ });
|
|
87
|
+
});
|
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
|
|
3
|
+
const MAX_UINT256 = (1n << 256n) - 1n;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Encodes a bigint as a 0x-prefixed, 64-char lowercase hex string — the canonical text encoding of
|
|
7
|
+
* a `bytes32` value, used throughout this package for storage slots and similar 256-bit identifiers.
|
|
8
|
+
* Throws if the value is outside the 0..2^256-1 range, since that almost certainly indicates a bug
|
|
9
|
+
* upstream (overflow, wrong hash, etc.) and would silently produce a string longer than 64 chars.
|
|
10
|
+
*
|
|
11
|
+
* @param value bigint value to encode
|
|
12
|
+
* @returns 0x-prefixed, 64-char lowercase hex string
|
|
13
|
+
*/
|
|
14
|
+
export function toBytes32Hex(value: bigint): string {
|
|
15
|
+
if (value < 0n || value > MAX_UINT256) {
|
|
16
|
+
throw new Error(`Value out of uint256 range: ${value}`);
|
|
17
|
+
}
|
|
18
|
+
return `0x${value.toString(16).padStart(64, '0')}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
22
|
+
* Parses a Solidity unsigned integer literal source string (per
|
|
23
|
+
* https://docs.soliditylang.org/en/latest/types.html#rational-literals) and returns it in the
|
|
24
|
+
* canonical hex form produced by {@link toBytes32Hex}.
|
|
6
25
|
*
|
|
7
|
-
*
|
|
26
|
+
* Parsing is delegated to {@link BigNumber}, which accepts decimal (`1000`), hex (`0x1f`), and
|
|
27
|
+
* scientific notation (`1e18`). Underscore separators and uppercase digits, which BigNumber rejects
|
|
28
|
+
* but Solidity allows, are stripped/lowercased first. Non-integer results (e.g. `1.5e3`) are rejected.
|
|
8
29
|
*
|
|
9
|
-
* If the input is null or undefined, returns
|
|
30
|
+
* If the input is null or undefined, returns the canonical hex form of zero.
|
|
10
31
|
*
|
|
11
32
|
* Assumes the input is a valid uint256-compatible literal for Solidity, and only performs minimal validation.
|
|
12
33
|
*
|
|
13
|
-
* @param literal Integer literal to
|
|
14
|
-
* @returns
|
|
34
|
+
* @param literal Integer literal source string to parse
|
|
35
|
+
* @returns 0x-prefixed, 64-char lowercase hex string
|
|
15
36
|
*/
|
|
16
37
|
export function normalizeUint256Literal(literal: string | null | undefined): string {
|
|
17
38
|
if (!literal) {
|
|
18
|
-
return
|
|
39
|
+
return toBytes32Hex(0n);
|
|
19
40
|
}
|
|
20
41
|
const lowercaseNoUnderscores = literal.replace(/_/g, '').toLowerCase();
|
|
21
42
|
|
|
@@ -23,7 +44,5 @@ export function normalizeUint256Literal(literal: string | null | undefined): str
|
|
|
23
44
|
if (!parsed.isInteger()) {
|
|
24
45
|
throw new Error(`Invalid integer literal: ${literal}`);
|
|
25
46
|
}
|
|
26
|
-
|
|
27
|
-
const padded = calculatedValueAsHex.padStart(64, '0');
|
|
28
|
-
return `0x${padded}`;
|
|
47
|
+
return toBytes32Hex(BigInt(parsed.toFixed()));
|
|
29
48
|
}
|
|
@@ -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
|
+
});
|