Skip to content

Commit 857e73d

Browse files
committed
Using Slang's language inference tool
1 parent 50c9115 commit 857e73d

2 files changed

Lines changed: 67 additions & 146 deletions

File tree

src/slang-utils/create-parser.ts

Lines changed: 51 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,79 @@
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 { maxSatisfying } 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();
2011

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;
12+
// This list was retrieved from Slang's documentation.
13+
// https://nomicfoundation.github.io/slang/latest/solidity-grammar/supported-versions/
14+
// TODO: use the run-time functionality if NomicFoundation decides to expose
15+
// this information directly.
16+
const milestoneVersions = [
17+
'0.4.14',
18+
'0.4.16',
19+
'0.4.21',
20+
'0.4.22',
21+
'0.4.25',
22+
'0.5.0',
23+
'0.5.3',
24+
'0.5.5',
25+
'0.5.8',
26+
'0.5.10',
27+
'0.5.14',
28+
'0.6.0',
29+
'0.6.2',
30+
'0.6.5',
31+
'0.6.7',
32+
'0.6.8',
33+
'0.6.11',
34+
'0.7.0',
35+
'0.7.1',
36+
'0.7.4',
37+
'0.8.0',
38+
'0.8.4',
39+
'0.8.8',
40+
'0.8.13',
41+
'0.8.18',
42+
'0.8.19',
43+
'0.8.22',
44+
'0.8.27',
45+
'0.8.29'
46+
].map((milestone) => maxSatisfying(supportedVersions, `<${milestone}`)!);
3947

4048
export function createParser(
4149
text: string,
4250
options: ParserOptions<AstNode>
4351
): [Parser, ParseOutput] {
52+
let parser: Parser;
4453
const compiler = maxSatisfying(supportedVersions, options.compiler);
4554
if (compiler) {
46-
if (!parser || parser.languageVersion !== compiler) {
47-
parser = Parser.create(compiler);
48-
}
55+
parser = Parser.create(compiler);
4956
return [parser, parser.parseNonterminal(NonterminalKind.SourceUnit, text)];
5057
}
5158

52-
let isCachedParser = false;
53-
if (parser) {
54-
isCachedParser = true;
55-
} else {
56-
parser = Parser.create(milestoneVersions[0]);
57-
}
58-
5959
let parseOutput;
60-
let inferredRanges: string[] = [];
6160

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-
}
61+
const inferredRanges: string[] = LanguageFacts.inferLanguageVersions(text);
62+
parser = Parser.create(inferredRanges[inferredRanges.length - 1]);
63+
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
8264

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)
92-
);
93-
},
94-
supportedVersions
95-
);
65+
if (parseOutput.isValid()) return [parser, parseOutput];
9666

97-
const inferredVersion =
98-
satisfyingVersions.length > 0
99-
? satisfyingVersions[satisfyingVersions.length - 1]
100-
: supportedVersions[supportedVersions.length - 1];
67+
const inferredMilestones = milestoneVersions.filter((milestone) =>
68+
inferredRanges.includes(milestone)
69+
);
10170

102-
if (inferredVersion !== parser.languageVersion) {
103-
parser = Parser.create(inferredVersion);
71+
for (let i = inferredMilestones.length - 1; i > 0; i -= 1) {
72+
const version = inferredMilestones[i];
73+
parser = Parser.create(version);
10474
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
75+
if (parseOutput.isValid()) break;
10576
}
10677

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-
)
130-
);
131-
}
132-
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.
139-
throw new Error(
140-
`Using version ${parser.languageVersion} did not find any pragma statement and does not parse without errors.`
141-
);
142-
}
143-
144-
return ranges;
78+
return [parser, parseOutput];
14579
}

tests/unit/slang-utils/create-parser.test.js

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,25 @@ describe('inferLanguage', function () {
1919
{
2020
description: 'With nightly commit',
2121
source: `pragma solidity 0.8.18-ci.2023.1.17+commit.e7b959af;`,
22-
version: '0.8.18'
22+
version: '0.8.18',
23+
// TODO: unskip this test when addresses this issue
24+
// https://github.com/NomicFoundation/slang/issues/1346
25+
skip: true
2326
},
2427
{
2528
description: 'Caret range and pinned version',
2629
source: `pragma solidity ^0.8.0; pragma solidity 0.8.2;`,
2730
version: '0.8.2'
2831
},
32+
{
33+
description: 'pragma is broken by new lines, whitespace and comments',
34+
source: `pragma solidity 0.
35+
// comment 1
36+
7.
37+
/* comment 2*/
38+
3;`,
39+
version: '0.7.3'
40+
},
2941
{
3042
description: 'With multiline comment before the range',
3143
source: `pragma solidity /* comment */ 0.8.2;`,
@@ -76,8 +88,8 @@ describe('inferLanguage', function () {
7688
}
7789
];
7890

79-
for (const { description, source, version } of fixtures) {
80-
test(description, function () {
91+
for (const { description, source, version, skip } of fixtures) {
92+
(skip ? test.skip : test)(description, function () {
8193
const [parser] = createParser(source, options);
8294
expect(parser.languageVersion).toEqual(version);
8395
});
@@ -113,32 +125,7 @@ describe('inferLanguage', function () {
113125
expect(parser.languageVersion).toEqual(latestSupportedVersion);
114126
});
115127

116-
test('should throw when a pragma is broken by new lines, whitespace and comments', function () {
117-
expect(() =>
118-
createParser(
119-
`pragma solidity 0.
120-
// comment 1
121-
7.
122-
/* comment 2*/
123-
3;`,
124-
options
125-
)
126-
).toThrow(
127-
"Couldn't infer any version from the ranges in the pragmas for file test.sol"
128-
);
129-
expect(() =>
130-
createParser(
131-
`pragma solidity 0.
132-
// comment 1
133-
7.
134-
/* comment 2*/
135-
3;`,
136-
{}
137-
)
138-
).toThrow("Couldn't infer any version from the ranges in the pragmas");
139-
});
140-
141-
test.skip('should throw an error if there are incompatible ranges', function () {
128+
test('should throw an error if there are incompatible ranges', function () {
142129
expect(() =>
143130
createParser(`pragma solidity ^0.8.0; pragma solidity 0.7.6;`, options)
144131
).toThrow();

0 commit comments

Comments
 (0)