Skip to content

Commit 3c24b79

Browse files
authored
Using Slang's language inference tool (#1158)
* updating dependencies * Using Slang's language inference tool * Fixing test with conflicting pragmas and syntax * returning an object instead of an array for clarity * adding some documentation and fixing a descending for loop condition * removing unnecessary constant * small refactor for DRY code * removing the logic that added issues for maintainance * improving the error messages when the language inference fails because of the syntax. * using the earliest version with the exception when there are no pragma statements * passing tests * Fix typos and adding a check for an empty array
1 parent f9e083b commit 3c24b79

30 files changed

Lines changed: 142 additions & 245 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,14 @@
8585
"devDependencies": {
8686
"@babel/code-frame": "^7.27.1",
8787
"@eslint/eslintrc": "^3.3.1",
88-
"@eslint/js": "^9.28.0",
88+
"@eslint/js": "^9.29.0",
8989
"@types/jest": "^29.5.14",
9090
"@types/semver": "^7.7.0",
91-
"@typescript-eslint/eslint-plugin": "^8.34.0",
92-
"@typescript-eslint/parser": "^8.34.0",
91+
"@typescript-eslint/eslint-plugin": "^8.34.1",
92+
"@typescript-eslint/parser": "^8.34.1",
9393
"c8": "^10.1.3",
9494
"cross-env": "^7.0.3",
95-
"eslint": "^9.28.0",
95+
"eslint": "^9.29.0",
9696
"eslint-config-prettier": "^10.1.5",
9797
"esm-utils": "^4.4.2",
9898
"globals": "^16.2.0",

src/slang-utils/create-parser.ts

Lines changed: 41 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,66 @@
1-
import { VersionExpressionSets as SlangVersionExpressionSets } from '@nomicfoundation/slang/ast';
2-
import { NonterminalKind, Query } from '@nomicfoundation/slang/cst';
1+
import { NonterminalKind } from '@nomicfoundation/slang/cst';
32
import { Parser } from '@nomicfoundation/slang/parser';
43
import { LanguageFacts } from '@nomicfoundation/slang/utils';
5-
import {
6-
maxSatisfying,
7-
minSatisfying,
8-
minor,
9-
major,
10-
satisfies,
11-
validRange
12-
} from 'semver';
13-
import { VersionExpressionSets } from '../slang-nodes/VersionExpressionSets.js';
4+
import { minSatisfying } from 'semver';
145

156
import type { ParseOutput } from '@nomicfoundation/slang/parser';
167
import type { ParserOptions } from 'prettier';
178
import type { AstNode } from '../slang-nodes/types.js';
189

1910
const supportedVersions = LanguageFacts.allVersions();
11+
const supportedLength = supportedVersions.length;
2012

21-
const milestoneVersions = Array.from(
22-
supportedVersions.reduce((minorRanges, version) => {
23-
minorRanges.add(`^${major(version)}.${minor(version)}.0`);
24-
return minorRanges;
25-
}, new Set<string>())
26-
)
27-
.reverse()
28-
.reduce((versions: string[], range) => {
29-
versions.push(maxSatisfying(supportedVersions, range)!);
30-
versions.push(minSatisfying(supportedVersions, range)!);
31-
return versions;
32-
}, []);
33-
34-
const queries = [
35-
Query.create('[VersionPragma @versionRanges [VersionExpressionSets]]')
36-
];
37-
38-
let parser: Parser;
13+
function parserAndOutput(
14+
text: string,
15+
version: string
16+
): { parser: Parser; parseOutput: ParseOutput } {
17+
const parser = Parser.create(version);
18+
return {
19+
parser,
20+
parseOutput: parser.parseNonterminal(NonterminalKind.SourceUnit, text)
21+
};
22+
}
3923

4024
export function createParser(
4125
text: string,
4226
options: ParserOptions<AstNode>
43-
): [Parser, ParseOutput] {
44-
const compiler = maxSatisfying(supportedVersions, options.compiler);
27+
): { parser: Parser; parseOutput: ParseOutput } {
28+
const compiler = minSatisfying(supportedVersions, options.compiler);
4529
if (compiler) {
46-
if (!parser || parser.languageVersion !== compiler) {
47-
parser = Parser.create(compiler);
48-
}
49-
return [parser, parser.parseNonterminal(NonterminalKind.SourceUnit, text)];
50-
}
51-
52-
let isCachedParser = false;
53-
if (parser) {
54-
isCachedParser = true;
55-
} else {
56-
parser = Parser.create(milestoneVersions[0]);
57-
}
58-
59-
let parseOutput;
60-
let inferredRanges: string[] = [];
61-
62-
try {
63-
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
64-
inferredRanges = tryToCollectPragmas(parseOutput, parser, isCachedParser);
65-
} catch {
66-
for (
67-
let i = isCachedParser ? 0 : 1;
68-
i <= milestoneVersions.length;
69-
i += 1
70-
) {
71-
try {
72-
const version = milestoneVersions[i];
73-
parser = Parser.create(version);
74-
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
75-
inferredRanges = tryToCollectPragmas(parseOutput, parser);
76-
break;
77-
} catch {
78-
continue;
79-
}
80-
}
81-
}
82-
83-
const satisfyingVersions = inferredRanges.reduce(
84-
(versions, inferredRange) => {
85-
if (!validRange(inferredRange)) {
86-
throw new Error(
87-
`Couldn't infer any version from the ranges in the pragmas${options.filepath ? ` for file ${options.filepath}` : ''}`
88-
);
89-
}
90-
return versions.filter((supportedVersion) =>
91-
satisfies(supportedVersion, inferredRange)
30+
const result = parserAndOutput(text, compiler);
31+
32+
if (!result.parseOutput.isValid())
33+
throw new Error(
34+
`We encountered the following syntax error:\n\n\t${
35+
result.parseOutput.errors()[0].message
36+
}\n\nBased on the compiler option provided, we inferred your code to be using Solidity version ${
37+
result.parser.languageVersion
38+
}. If you would like to change that, specify a different version in your \`.prettierrc\` file.`
9239
);
93-
},
94-
supportedVersions
95-
);
96-
97-
const inferredVersion =
98-
satisfyingVersions.length > 0
99-
? satisfyingVersions[satisfyingVersions.length - 1]
100-
: supportedVersions[supportedVersions.length - 1];
10140

102-
if (inferredVersion !== parser.languageVersion) {
103-
parser = Parser.create(inferredVersion);
104-
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
41+
return result;
10542
}
43+
const inferredRanges: string[] = LanguageFacts.inferLanguageVersions(text);
44+
const inferredLength = inferredRanges.length;
10645

107-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
108-
return [parser, parseOutput!];
109-
}
110-
111-
function tryToCollectPragmas(
112-
parseOutput: ParseOutput,
113-
parser: Parser,
114-
isCachedParser = false
115-
): string[] {
116-
const matches = parseOutput.createTreeCursor().query(queries);
117-
const ranges: string[] = [];
118-
119-
let match;
120-
while ((match = matches.next())) {
121-
const versionRange = new SlangVersionExpressionSets(
122-
match.captures.versionRanges[0].node.asNonterminalNode()!
123-
);
124-
ranges.push(
125-
// Replace all comments that could be in the expression with whitespace
126-
new VersionExpressionSets(versionRange).comments.reduce(
127-
(range, comment) => range.replace(comment.value, ' '),
128-
versionRange.cst.unparse()
129-
)
46+
if (inferredLength === 0) {
47+
throw new Error(
48+
`We couldn't infer a Solidity version base on the pragma statements in your code. If you would like to change that, update the pragmas in your source file, or specify a version in your \`.prettierrc\` file.`
13049
);
13150
}
51+
const result = parserAndOutput(
52+
text,
53+
inferredRanges[inferredLength === supportedLength ? inferredLength - 1 : 0]
54+
);
13255

133-
if (ranges.length === 0) {
134-
// If we didn't find pragmas but succeeded parsing the source we keep it.
135-
if (!isCachedParser && parseOutput.isValid()) {
136-
return [parser.languageVersion];
137-
}
138-
// Otherwise we throw.
56+
if (!result.parseOutput.isValid())
13957
throw new Error(
140-
`Using version ${parser.languageVersion} did not find any pragma statement and does not parse without errors.`
58+
`We encountered the following syntax error:\n\n\t${
59+
result.parseOutput.errors()[0].message
60+
}\n\nBased on the pragma statements, we inferred your code to be using Solidity version ${
61+
result.parser.languageVersion
62+
}. If you would like to change that, update the pragmas in your source file, or specify a version in your \`.prettierrc\` file.`
14163
);
142-
}
14364

144-
return ranges;
65+
return result;
14566
}

src/slangSolidityParser.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@ export default function parse(
1111
text: string,
1212
options: ParserOptions<AstNode>
1313
): AstNode {
14-
const [parser, parseOutput] = createParser(text, options);
14+
const { parser, parseOutput } = createParser(text, options);
1515

16-
if (parseOutput.isValid()) {
17-
// We update the compiler version by the inferred one.
18-
options.compiler = parser.languageVersion;
19-
const parsed = new SourceUnit(
20-
new SlangSourceUnit(parseOutput.tree.asNonterminalNode()),
21-
options
22-
);
23-
clearOffsets();
24-
return parsed;
25-
}
26-
throw new Error(parseOutput.errors()[0].message);
16+
// We update the compiler version by the inferred one.
17+
options.compiler = parser.languageVersion;
18+
const parsed = new SourceUnit(
19+
new SlangSourceUnit(parseOutput.tree.asNonterminalNode()),
20+
options
21+
);
22+
clearOffsets();
23+
return parsed;
2724
}

tests/config/run-format-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ async function runTest({
334334
// same for ANTLR unless it was already given as an option.
335335
compiler:
336336
formatOptions.compiler ||
337-
createParser(code, formatOptions)[0].languageVersion,
337+
createParser(code, formatOptions).parser.languageVersion,
338338
parser: "antlr",
339339
plugins: await getPlugins(),
340340
});

tests/format/AllSolidityFeatures/AllSolidityFeatures.sol

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
// Examples taken from the Solidity documentation online.
2-
3-
// for pragma version numbers, see https://docs.npmjs.com/misc/semver#versions
4-
pragma solidity 0.4.0;
5-
pragma solidity ^0.4.0;
62
pragma experimental ABIEncoderV2;
73

84
import "SomeFile.sol";

tests/format/AllSolidityFeatures/__snapshots__/format.test.js.snap

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ printWidth: 80
77
| printWidth
88
=====================================input======================================
99
// Examples taken from the Solidity documentation online.
10-
11-
// for pragma version numbers, see https://docs.npmjs.com/misc/semver#versions
12-
pragma solidity 0.4.0;
13-
pragma solidity ^0.4.0;
1410
pragma experimental ABIEncoderV2;
1511
1612
import "SomeFile.sol";
@@ -370,10 +366,6 @@ using { add as + , sub } for Fixed18 global ;
370366
371367
=====================================output=====================================
372368
// Examples taken from the Solidity documentation online.
373-
374-
// for pragma version numbers, see https://docs.npmjs.com/misc/semver#versions
375-
pragma solidity 0.4.0;
376-
pragma solidity ^0.4.0;
377369
pragma experimental ABIEncoderV2;
378370
379371
import "SomeFile.sol";

tests/format/AllSolidityFeaturesV0.4.26/__snapshots__/format.test.js.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// Jest Snapshot v1, https://goo.gl/fbAQLP
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

3-
exports[`AllSolidityFeatures.sol format 1`] = `
3+
exports[`AllSolidityFeatures.sol - {"compiler":"0.4.26"} format 1`] = `
44
====================================options=====================================
5+
compiler: "0.4.26"
56
parsers: ["slang"]
67
printWidth: 80
78
| printWidth
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
runFormatTest(import.meta, ['slang']);
1+
runFormatTest(import.meta, ['slang'], { compiler: '0.4.26' });

tests/format/BasicIterator/__snapshots__/format.test.js.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// Jest Snapshot v1, https://goo.gl/fbAQLP
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

3-
exports[`BasicIterator.sol format 1`] = `
3+
exports[`BasicIterator.sol - {"compiler":"0.4.26"} format 1`] = `
44
====================================options=====================================
5+
compiler: "0.4.26"
56
parsers: ["slang"]
67
printWidth: 80
78
| printWidth

0 commit comments

Comments
 (0)