Skip to content

Commit eb97aef

Browse files
Amxxfrangio
andauthored
Disable transpilation of contracts with @Custom:stateless in peerProject mode (#132)
Co-authored-by: Francisco <[email protected]>
1 parent 5140239 commit eb97aef

12 files changed

+112
-82
lines changed

contracts/project/SomeContract.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity ^0.8.0;
33

44
import { ISomeInterface } from "./ISomeInterface.sol";
55
import { SomeLibrary } from "./SomeLibrary.sol";
6+
import { SomeStatelessContract } from "./SomeStatelessContract.sol";
67

78
interface ISomeContract is ISomeInterface {}
89

@@ -17,11 +18,11 @@ struct SomeStruct {
1718
contract SomeContract is ISomeContract {
1819
SomeStruct s;
1920

20-
function someFunction() public override returns (bool) {
21+
function someFunction() public pure override returns (bool) {
2122
return false;
2223
}
2324

24-
function someOtherFunction() public override returns (bool) {
25+
function someOtherFunction() public pure override returns (bool) {
2526
return true;
2627
}
2728

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
/// @custom:stateless
5+
contract SomeStatelessContract {}

src/index.ts

Lines changed: 5 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import fs from 'fs';
33
import { mapValues } from 'lodash';
44
import { minimatch } from 'minimatch';
55

6-
import { Node } from 'solidity-ast/node';
76
import { matcher } from './utils/matcher';
87
import { renamePath, isRenamed } from './rename';
98
import { SolcOutput, SolcInput } from './solc/input-output';
10-
import { Transform, TransformData } from './transform';
9+
import { Transform } from './transform';
1110
import { generateWithInit } from './generate-with-init';
1211
import { findAlreadyInitializable } from './find-already-initializable';
12+
import { preparePeerProject } from './prepare-peer-project';
1313

1414
import { fixImportDirectives } from './transformations/fix-import-directives';
1515
import { renameIdentifiers } from './transformations/rename-identifiers';
@@ -52,8 +52,6 @@ interface TranspileOptions {
5252
peerProject?: string;
5353
}
5454

55-
type NodeTransformData = { node: Node; data: Partial<TransformData> };
56-
5755
function getExtraOutputPaths(
5856
paths: Paths,
5957
options: TranspileOptions = {},
@@ -73,64 +71,16 @@ function getExtraOutputPaths(
7371
return outputPaths;
7472
}
7573

76-
function getExcludeAndImportPathsForPeer(
77-
solcOutput: SolcOutput,
78-
peerProject: string,
79-
): [Set<string>, NodeTransformData[]] {
80-
const data: NodeTransformData[] = [];
81-
const exclude: Set<string> = new Set();
82-
83-
for (const [source, { ast }] of Object.entries(solcOutput.sources)) {
84-
let shouldExclude = true;
85-
for (const node of ast.nodes) {
86-
switch (node.nodeType) {
87-
case 'ContractDefinition': {
88-
if (node.contractKind === 'contract') {
89-
shouldExclude = false;
90-
} else {
91-
const importFromPeer = path.join(peerProject, source);
92-
data.push({ node, data: { importFromPeer } });
93-
}
94-
break;
95-
}
96-
case 'EnumDefinition':
97-
case 'ErrorDefinition':
98-
case 'FunctionDefinition':
99-
case 'StructDefinition':
100-
case 'UserDefinedValueTypeDefinition':
101-
case 'VariableDeclaration': {
102-
const importFromPeer = path.join(peerProject, source);
103-
data.push({ node, data: { importFromPeer } });
104-
break;
105-
}
106-
case 'ImportDirective':
107-
case 'PragmaDirective':
108-
case 'UsingForDirective': {
109-
break;
110-
}
111-
}
112-
}
113-
if (shouldExclude) {
114-
exclude.add(source);
115-
}
116-
}
117-
118-
return [exclude, data];
119-
}
120-
12174
export async function transpile(
12275
solcInput: SolcInput,
12376
solcOutput: SolcOutput,
12477
paths: Paths,
12578
options: TranspileOptions = {},
12679
): Promise<OutputFile[]> {
127-
const nodeData: NodeTransformData[] = [];
128-
12980
const outputPaths = getExtraOutputPaths(paths, options);
13081
const alreadyInitializable = findAlreadyInitializable(solcOutput, options.initializablePath);
13182

13283
const excludeSet = new Set([...alreadyInitializable, ...Object.values(outputPaths)]);
133-
const softExcludeSet = new Set();
13484
const excludeMatch = matcher(options.exclude ?? []);
13585

13686
const namespaceInclude = (source: string) => {
@@ -139,27 +89,12 @@ export async function transpile(
13989
return namespaced && !namespaceExclude.some(p => minimatch(source, p));
14090
};
14191

142-
// if partial transpilation, extract the list of soft exclude, and the peer import paths.
143-
if (options.peerProject !== undefined) {
144-
const [peerSoftExcludeSet, importFromPeerData] = getExcludeAndImportPathsForPeer(
145-
solcOutput,
146-
options.peerProject,
147-
);
148-
peerSoftExcludeSet.forEach(source => softExcludeSet.add(source));
149-
nodeData.push(...importFromPeerData);
150-
}
151-
15292
const transform = new Transform(solcInput, solcOutput, {
153-
exclude: source =>
154-
excludeSet.has(source) || (excludeMatch(source) ?? isRenamed(source))
155-
? 'hard'
156-
: softExcludeSet.has(source)
157-
? 'soft'
158-
: false,
93+
exclude: source => excludeSet.has(source) || (excludeMatch(source) ?? isRenamed(source)),
15994
});
16095

161-
for (const { node, data } of nodeData) {
162-
Object.assign(transform.getData(node), data);
96+
if (options.peerProject !== undefined) {
97+
preparePeerProject(transform, options.peerProject);
16398
}
16499

165100
transform.apply(renameIdentifiers);

src/partial-transform.test.ts.snap

-869 Bytes
Binary file not shown.

src/partial-transform.test.ts renamed to src/prepare-peer-project.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import _test, { TestFn } from 'ava';
22
import hre from 'hardhat';
3+
import path from 'path';
34

45
import { getBuildInfo } from './test-utils/get-build-info';
56
import { OutputFile, transpile } from '.';
@@ -8,6 +9,7 @@ import { SolcOutput } from './solc/input-output';
89
const test = _test as TestFn<Context>;
910

1011
interface Context {
12+
inputs: string[];
1113
files: OutputFile[];
1214
}
1315

@@ -20,23 +22,31 @@ test.serial.before('compile', async t => {
2022
const solcOutput = buildInfo.output as SolcOutput;
2123
const exclude = Object.keys(solcOutput.sources).filter(path => !path.startsWith(projectDir));
2224

25+
t.context.inputs = Object.keys(solcInput.sources);
2326
t.context.files = await transpile(solcInput, solcOutput, hre.config.paths, {
2427
exclude,
2528
peerProject,
2629
});
2730
});
2831

29-
for (const fileName of ['ISomeInterface.sol', 'SomeLibrary.sol']) {
32+
for (const fileName of ['ISomeInterface.sol', 'SomeLibrary.sol', 'SomeStatelessContract.sol']) {
3033
test(`do not transpile ${fileName}`, t => {
3134
const file = t.context.files.find(f => f.fileName === fileName);
35+
// source file exists
36+
t.true(t.context.inputs.includes(path.join(projectDir, fileName)));
37+
// transpiled file does not exist
3238
t.is(file, undefined, 'file should not be transpiled');
3339
});
3440
}
3541

3642
for (const fileName of ['SomeContract.sol', 'SomeOtherContract.sol']) {
3743
test(`transpile ${fileName}`, t => {
3844
const file = t.context.files.find(f => f.fileName === fileName);
45+
// source file exists
46+
t.true(t.context.inputs.includes(path.join(projectDir, fileName)));
47+
// transpiled file exists
3948
t.not(file, undefined, 'file not found');
49+
// snapshot
4050
t.snapshot(file);
4151
});
4252
}

src/partial-transform.test.ts.md renamed to src/prepare-peer-project.test.ts.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Generated by [AVA](https://avajs.dev).
1616
1717
import {ISomeInterface} from "@openzeppelin/contracts/project/ISomeInterface.sol";␊
1818
import {SomeLibrary} from "@openzeppelin/contracts/project/SomeLibrary.sol";␊
19+
import {SomeStatelessContract} from "@openzeppelin/contracts/project/SomeStatelessContract.sol";␊
1920
import {Initializable} from "../Initializable.sol";␊
2021
2122
import { ISomeContract } from "@openzeppelin/contracts/project/SomeContract.sol";␊
@@ -34,11 +35,11 @@ Generated by [AVA](https://avajs.dev).
3435
3536
function __SomeContract_init_unchained() internal onlyInitializing {␊
3637
}␊
37-
function someFunction() public override returns (bool) {␊
38+
function someFunction() public pure override returns (bool) {␊
3839
return false;␊
3940
}␊
4041
41-
function someOtherFunction() public override returns (bool) {␊
42+
function someOtherFunction() public pure override returns (bool) {␊
4243
return true;␊
4344
}␊
4445

src/prepare-peer-project.test.ts.snap

883 Bytes
Binary file not shown.

src/prepare-peer-project.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import path from 'path';
2+
3+
import { Transform } from './transform';
4+
import { extractContractStorageSize, extractContractStateless } from './utils/natspec';
5+
import { isStorageVariable } from './transformations/utils/is-storage-variable';
6+
7+
export function preparePeerProject(transform: Transform, peerProject: string) {
8+
for (const ast of transform.asts()) {
9+
let shouldExclude = true;
10+
for (const node of ast.nodes) {
11+
switch (node.nodeType) {
12+
case 'ContractDefinition': {
13+
if (node.contractKind === 'contract') {
14+
if (!extractContractStateless(node)) {
15+
shouldExclude = false;
16+
break;
17+
}
18+
if (extractContractStorageSize(node) !== undefined) {
19+
throw transform.error(
20+
node,
21+
'Contract marked as stateless should not have an associated storage size',
22+
);
23+
}
24+
for (const decl of node.nodes) {
25+
if (
26+
decl.nodeType == 'VariableDeclaration' &&
27+
isStorageVariable(decl, transform.resolver)
28+
) {
29+
throw transform.error(
30+
node,
31+
'Contract marked as stateless should not contain storage variable declarations',
32+
);
33+
}
34+
if (decl.nodeType == 'FunctionDefinition' && decl.kind == 'constructor') {
35+
throw transform.error(
36+
node,
37+
'Contract marked as stateless should not have a constructor',
38+
);
39+
}
40+
}
41+
}
42+
transform.getData(node).importFromPeer = path.join(peerProject, ast.absolutePath);
43+
break;
44+
}
45+
case 'EnumDefinition':
46+
case 'ErrorDefinition':
47+
case 'FunctionDefinition':
48+
case 'StructDefinition':
49+
case 'UserDefinedValueTypeDefinition':
50+
case 'VariableDeclaration': {
51+
transform.getData(node).importFromPeer = path.join(peerProject, ast.absolutePath);
52+
break;
53+
}
54+
case 'ImportDirective':
55+
case 'PragmaDirective':
56+
case 'UsingForDirective': {
57+
break;
58+
}
59+
}
60+
}
61+
if (shouldExclude) {
62+
transform.exclude(ast.absolutePath);
63+
}
64+
}
65+
}

src/transform-namespaces.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test.serial.before('compile', async t => {
3030
test.beforeEach('transform', async t => {
3131
t.context.transformFile = (file: string) =>
3232
new Transform(t.context.solcInput, t.context.solcOutput, {
33-
exclude: source => (source !== file ? 'hard' : false),
33+
exclude: source => source !== file,
3434
});
3535
});
3636

src/transform.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ test.serial.before('compile', async t => {
3939

4040
t.context.transformFile = (file: string) =>
4141
new Transform(t.context.solcInput, t.context.solcOutput, {
42-
exclude: source => (source !== file ? 'hard' : false),
42+
exclude: source => source !== file,
4343
});
4444
});
4545

4646
test.beforeEach('transform', async t => {
4747
t.context.transform = new Transform(t.context.solcInput, t.context.solcOutput, {
48-
exclude: source => (source.startsWith('contracts/invalid/') ? 'hard' : false),
48+
exclude: source => source.startsWith('contracts/invalid/'),
4949
});
5050
});
5151

@@ -191,7 +191,7 @@ test('fix new statement in var init', t => {
191191
test('exclude', t => {
192192
const file = 'contracts/TransformInitializable.sol';
193193
const transform = new Transform(t.context.solcInput, t.context.solcOutput, {
194-
exclude: s => (s === file ? 'hard' : false),
194+
exclude: s => s === file,
195195
});
196196
// eslint-disable-next-line require-yield
197197
transform.apply(function* (s) {

0 commit comments

Comments
 (0)