Skip to content

Commit 04a0b6d

Browse files
fix: constructor validation on build (#226)
* fix: constructor validation on build * Update unordered-map.js * feat: constructor checker * feat: constructor validation * fix: resolve conflicts * fix: resolve conflicts * fix: code cleaning * fix: code cleaning * fix: code cleaning * fix: lint * fix: tests * fix: empty state contract * fix: update contract valdiation to handle clean-state example * linter fix * constructor validation moved to utils * constructor test contracts renamed, typos fixed * redandunt comment deleted * Error messages changed * log comments added to contract validation * yarn build * cli/cli.js deleted * contract validation added to the main pipeline * contract validation rewrittent with TS * signal used in contract validation, tests rewritten * contract validation doc added Co-authored-by: Serhii Volovyk <[email protected]>
1 parent ef1a7b2 commit 04a0b6d

16 files changed

+2099
-1101
lines changed

Diff for: examples/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@types/lodash-es": "^4.17.6",
4444
"ava": "^4.2.0",
4545
"near-workspaces": "3.2.1",
46-
"typescript": "^4.7.4"
46+
"typescript": "^4.7.4",
47+
"npm-run-all": "^4.1.5"
4748
}
4849
}

Diff for: examples/yarn.lock

+927-507
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: lib/cli/cli.js

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: lib/cli/utils.d.ts

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: lib/cli/utils.js

+44
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
"@types/eslint": "^8.4.6",
6262
"@types/node": "^17.0.38",
6363
"@types/rollup": "^0.54.0",
64+
"chalk": "^5.1.0",
65+
"ts-morph": "^16.0.0",
6466
"@types/signale": "^1.4.4",
6567
"@typescript-eslint/eslint-plugin": "^5.37.0",
6668
"@typescript-eslint/parser": "^5.37.0",

Diff for: src/cli/cli.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { rollup } from "rollup";
1010
import { Command } from "commander";
1111
import signal from "signale";
1212

13-
import { executeCommand } from "./utils.js";
13+
import { executeCommand, validateContract } from "./utils.js";
1414

1515
const { Signale } = signal;
1616
const PROJECT_DIR = process.cwd();
@@ -66,6 +66,9 @@ export async function buildCom(
6666
signale.await(`Creating ${TARGET_DIR} directory...`);
6767
await executeCommand(`mkdir -p ${TARGET_DIR}`, verbose);
6868

69+
signal.await(`Validatig ${source} contract...`);
70+
await validateContract(source);
71+
6972
signale.await(`Creating ${source} file with Rollup...`);
7073
await createJsFileWithRullup(source, ROLLUP_TARGET, verbose);
7174

Diff for: src/cli/utils.ts

+48
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import childProcess from "child_process";
22
import { promisify } from "util";
33
import signal from "signale"
4+
import { ClassDeclaration, ClassDeclarationStructure, ConstructorDeclaration, OptionalKind, Project, PropertyDeclarationStructure, SourceFile } from "ts-morph";
5+
import chalk from "chalk";
6+
import signale from "signale";
47

58
const {Signale} = signal;
69

@@ -37,3 +40,48 @@ export async function executeCommand(
3740
export async function download(url: string, verbose = false) {
3841
await executeCommand(`curl -LOf ${url}`, verbose);
3942
}
43+
44+
const UNINITIALIZED_PARAMETERS_ERROR = "All parameters must be initialized in the constructor. Uninitialized parameters:";
45+
46+
/**
47+
* Validates the contract by checking that all parameters are initialized in the constructor. Works only for contracts written in TypeScript.
48+
* @param contractPath Path to the contract.
49+
**/
50+
export async function validateContract(contractPath: string): Promise<boolean> {
51+
const project: Project = new Project();
52+
project.addSourceFilesAtPaths(contractPath);
53+
const sourceFile: SourceFile = project.getSourceFile(contractPath);
54+
const classDeclarations: ClassDeclaration[] = sourceFile.getClasses();
55+
for (const classDeclaration of classDeclarations) {
56+
const classStructure: ClassDeclarationStructure = classDeclaration.getStructure();
57+
const { decorators, properties } = classStructure;
58+
const hasNearBindgen: boolean = decorators.find(
59+
(decorator) => decorator.name === "NearBindgen"
60+
) ? true : false;
61+
if (hasNearBindgen) {
62+
const constructors: ConstructorDeclaration[] = classDeclaration.getConstructors();
63+
const hasConstructor = constructors.length > 0;
64+
const propertiesToBeInited: OptionalKind<PropertyDeclarationStructure>[] = properties.filter((p) => !p.initializer);
65+
if (!hasConstructor && propertiesToBeInited.length === 0) {
66+
return true;
67+
}
68+
if (!hasConstructor && propertiesToBeInited.length > 0) {
69+
signale.error(chalk.redBright(`${UNINITIALIZED_PARAMETERS_ERROR} ${propertiesToBeInited.map((p) => p.name)}`));
70+
process.exit(1);
71+
}
72+
const constructor: ConstructorDeclaration = constructors[0];
73+
const constructorContent: string = constructor.getText();
74+
const nonInitedProperties: string[] = [];
75+
for (const property of propertiesToBeInited) {
76+
if (!constructorContent.includes(`this.${property.name}`)) {
77+
nonInitedProperties.push(property.name);
78+
}
79+
}
80+
if (nonInitedProperties.length > 0) {
81+
signale.error(chalk.redBright(`${UNINITIALIZED_PARAMETERS_ERROR} ${nonInitedProperties.join(", ")}`));
82+
process.exit(1);
83+
}
84+
}
85+
}
86+
return true;
87+
}

Diff for: tests/__tests__/constructor_validation.ava.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import test from "ava";
2+
import { execSync } from "child_process";
3+
4+
const BUILD_FAILURE_ERROR_CODE = 1;
5+
6+
test("should not throw error, constructor is correctly initialized", async (t) => {
7+
t.notThrows(() => {
8+
execSync(
9+
"near-sdk-js build src/constructor-validation/all-parameters-set-in-constructor.ts build/all-parameters-set-in-constructor.wasm"
10+
);
11+
});
12+
});
13+
14+
test("should throw error, name is not inited", async (t) => {
15+
const error = t.throws(() => {
16+
execSync(
17+
"near-sdk-js build src/constructor-validation/1-parameter-not-set-in-constructor.ts build/1-parameter-not-set-in-constructor.wasm"
18+
);
19+
});
20+
t.is(error.status, BUILD_FAILURE_ERROR_CODE);
21+
});
22+
23+
test("should throw error, construcor is empty", async (t) => {
24+
const error = t.throws(() => {
25+
execSync(
26+
"near-sdk-js build src/constructor-validation/no-parameters-set-in-constructor.ts build/no-parameters-set-in-constructor.wasm"
27+
);
28+
});
29+
t.is(error.status, BUILD_FAILURE_ERROR_CODE);
30+
});
31+
32+
test("should throw error, construcor is not declared", async (t) => {
33+
const error = t.throws(() => {
34+
execSync(
35+
"near-sdk-js build src/constructor-validation/no-constructor.ts build/no-constructor.wasm"
36+
);
37+
});
38+
t.is(error.status, BUILD_FAILURE_ERROR_CODE);
39+
});

Diff for: tests/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,15 @@
5151
"test:bigint-serialization": "ava __tests__/test-bigint-serialization.ava.js",
5252
"test:date-serialization": "ava __tests__/test-date-serialization.ava.js",
5353
"test:serialization": "ava __tests__/test-serialization.ava.js",
54+
"test:constructor-validation": "ava __tests__/constructor_validation.ava.js",
5455
"test:middlewares": "ava __tests__/test-middlewares.ava.js"
5556
},
5657
"author": "Near Inc <[email protected]>",
5758
"license": "Apache-2.0",
5859
"devDependencies": {
5960
"ava": "^4.2.0",
6061
"near-workspaces": "3.2.1",
61-
"typescript": "^4.7.4"
62+
"typescript": "^4.7.4",
63+
"npm-run-all": "^4.1.5"
6264
}
6365
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NearBindgen, LookupMap } from "near-sdk-js";
2+
3+
@NearBindgen({})
4+
export class ConstructorValidation {
5+
map: LookupMap<string>;
6+
name: string;
7+
constructor() {
8+
this.map = new LookupMap<string>("a");
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NearBindgen, LookupMap, call } from "near-sdk-js";
2+
3+
@NearBindgen({})
4+
export class ConstructorValidation {
5+
map: LookupMap<string>;
6+
name: string;
7+
constructor() {
8+
this.map = new LookupMap<string>("a");
9+
this.name = "";
10+
}
11+
12+
@call({})
13+
get() {
14+
return { status: "ok" };
15+
}
16+
}

Diff for: tests/src/constructor-validation/no-constructor.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { NearBindgen, LookupMap } from "near-sdk-js";
2+
3+
@NearBindgen({})
4+
export class ConstructorValidation {
5+
map: LookupMap<string>;
6+
name: string;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NearBindgen, LookupMap } from "near-sdk-js";
2+
3+
@NearBindgen({})
4+
export class ConstructorValidation {
5+
map: LookupMap<string>;
6+
name: string;
7+
constructor() {
8+
//
9+
}
10+
}

0 commit comments

Comments
 (0)