Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@
"devDependencies": {
"@babel/code-frame": "^7.27.1",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.28.0",
"@eslint/js": "^9.29.0",
"@types/jest": "^29.5.14",
"@types/semver": "^7.7.0",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
"c8": "^10.1.3",
"cross-env": "^7.0.3",
"eslint": "^9.28.0",
"eslint": "^9.29.0",
"eslint-config-prettier": "^10.1.5",
"esm-utils": "^4.4.2",
"globals": "^16.2.0",
Expand Down
160 changes: 38 additions & 122 deletions src/slang-utils/create-parser.ts
Original file line number Diff line number Diff line change
@@ -1,145 +1,61 @@
import { VersionExpressionSets as SlangVersionExpressionSets } from '@nomicfoundation/slang/ast';
import { NonterminalKind, Query } from '@nomicfoundation/slang/cst';
import { NonterminalKind } from '@nomicfoundation/slang/cst';
import { Parser } from '@nomicfoundation/slang/parser';
import { LanguageFacts } from '@nomicfoundation/slang/utils';
import {
maxSatisfying,
minSatisfying,
minor,
major,
satisfies,
validRange
} from 'semver';
import { VersionExpressionSets } from '../slang-nodes/VersionExpressionSets.js';
import { minSatisfying } from 'semver';

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

const supportedVersions = LanguageFacts.allVersions();
const supportedLength = supportedVersions.length;

const milestoneVersions = Array.from(
supportedVersions.reduce((minorRanges, version) => {
minorRanges.add(`^${major(version)}.${minor(version)}.0`);
return minorRanges;
}, new Set<string>())
)
.reverse()
.reduce((versions: string[], range) => {
versions.push(maxSatisfying(supportedVersions, range)!);
versions.push(minSatisfying(supportedVersions, range)!);
return versions;
}, []);

const queries = [
Query.create('[VersionPragma @versionRanges [VersionExpressionSets]]')
];

let parser: Parser;
function parserAndOutput(
text: string,
version: string
): { parser: Parser; parseOutput: ParseOutput } {
const parser = Parser.create(version);
return {
parser,
parseOutput: parser.parseNonterminal(NonterminalKind.SourceUnit, text)
};
}

export function createParser(
text: string,
options: ParserOptions<AstNode>
): [Parser, ParseOutput] {
const compiler = maxSatisfying(supportedVersions, options.compiler);
): { parser: Parser; parseOutput: ParseOutput } {
const compiler = minSatisfying(supportedVersions, options.compiler);
if (compiler) {
if (!parser || parser.languageVersion !== compiler) {
parser = Parser.create(compiler);
}
return [parser, parser.parseNonterminal(NonterminalKind.SourceUnit, text)];
}

let isCachedParser = false;
if (parser) {
isCachedParser = true;
} else {
parser = Parser.create(milestoneVersions[0]);
}

let parseOutput;
let inferredRanges: string[] = [];

try {
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
inferredRanges = tryToCollectPragmas(parseOutput, parser, isCachedParser);
} catch {
for (
let i = isCachedParser ? 0 : 1;
i <= milestoneVersions.length;
i += 1
) {
try {
const version = milestoneVersions[i];
parser = Parser.create(version);
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
inferredRanges = tryToCollectPragmas(parseOutput, parser);
break;
} catch {
continue;
}
}
}

const satisfyingVersions = inferredRanges.reduce(
(versions, inferredRange) => {
if (!validRange(inferredRange)) {
throw new Error(
`Couldn't infer any version from the ranges in the pragmas${options.filepath ? ` for file ${options.filepath}` : ''}`
);
}
return versions.filter((supportedVersion) =>
satisfies(supportedVersion, inferredRange)
const result = parserAndOutput(text, compiler);

if (!result.parseOutput.isValid())
throw new Error(
'We encoutered the following syntax error:\n\n\t' +
Copy link

Copilot AI Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word 'encoutered' appears to be misspelled; consider correcting it to 'encountered'.

Suggested change
'We encoutered the following syntax error:\n\n\t' +
'We encountered the following syntax error:\n\n\t' +

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I enabled the copilot review because I saw this typo and was curious if it would catch it and if it would catch something else.

result.parseOutput.errors()[0].message +
'\n\nBased on the compiler option provided, we inferred your code to be using Solidity version ' +
result.parser.languageVersion +
'. If you would like to change that, specify a different version in your `.prettierrc` file.'
);
},
supportedVersions
);

const inferredVersion =
satisfyingVersions.length > 0
? satisfyingVersions[satisfyingVersions.length - 1]
: supportedVersions[supportedVersions.length - 1];

if (inferredVersion !== parser.languageVersion) {
parser = Parser.create(inferredVersion);
parseOutput = parser.parseNonterminal(NonterminalKind.SourceUnit, text);
return result;
}
const inferredRanges: string[] = LanguageFacts.inferLanguageVersions(text);
const inferredLength = inferredRanges.length;

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return [parser, parseOutput!];
}

function tryToCollectPragmas(
parseOutput: ParseOutput,
parser: Parser,
isCachedParser = false
): string[] {
const matches = parseOutput.createTreeCursor().query(queries);
const ranges: string[] = [];

let match;
while ((match = matches.next())) {
const versionRange = new SlangVersionExpressionSets(
match.captures.versionRanges[0].node.asNonterminalNode()!
);
ranges.push(
// Replace all comments that could be in the expression with whitespace
new VersionExpressionSets(versionRange).comments.reduce(
(range, comment) => range.replace(comment.value, ' '),
versionRange.cst.unparse()
)
);
}
const result = parserAndOutput(
text,
inferredRanges[inferredLength === supportedLength ? inferredLength - 1 : 0]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can inferredRanges be empty? I think this can only happen if you have two pragma statements whose intersection is empty, which is very unlikely, but maybe throwing an assertion error wouldn't be a bad idea.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so far slang just throws an error in this case, but I could add an check here in case slang changes this behaviour

);

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

return ranges;
return result;
}
21 changes: 9 additions & 12 deletions src/slangSolidityParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ export default function parse(
text: string,
options: ParserOptions<AstNode>
): AstNode {
const [parser, parseOutput] = createParser(text, options);
const { parser, parseOutput } = createParser(text, options);

if (parseOutput.isValid()) {
// We update the compiler version by the inferred one.
options.compiler = parser.languageVersion;
const parsed = new SourceUnit(
new SlangSourceUnit(parseOutput.tree.asNonterminalNode()),
options
);
clearOffsets();
return parsed;
}
throw new Error(parseOutput.errors()[0].message);
// We update the compiler version by the inferred one.
options.compiler = parser.languageVersion;
const parsed = new SourceUnit(
new SlangSourceUnit(parseOutput.tree.asNonterminalNode()),
options
);
clearOffsets();
return parsed;
}
2 changes: 1 addition & 1 deletion tests/config/run-format-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ async function runTest({
// same for ANTLR unless it was already given as an option.
compiler:
formatOptions.compiler ||
createParser(code, formatOptions)[0].languageVersion,
createParser(code, formatOptions).parser.languageVersion,
parser: "antlr",
plugins: await getPlugins(),
});
Expand Down
4 changes: 0 additions & 4 deletions tests/format/AllSolidityFeatures/AllSolidityFeatures.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
// Examples taken from the Solidity documentation online.

// for pragma version numbers, see https://docs.npmjs.com/misc/semver#versions
pragma solidity 0.4.0;
pragma solidity ^0.4.0;
pragma experimental ABIEncoderV2;

import "SomeFile.sol";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ printWidth: 80
| printWidth
=====================================input======================================
// Examples taken from the Solidity documentation online.

// for pragma version numbers, see https://docs.npmjs.com/misc/semver#versions
pragma solidity 0.4.0;
pragma solidity ^0.4.0;
pragma experimental ABIEncoderV2;

import "SomeFile.sol";
Expand Down Expand Up @@ -370,10 +366,6 @@ using { add as + , sub } for Fixed18 global ;

=====================================output=====================================
// Examples taken from the Solidity documentation online.

// for pragma version numbers, see https://docs.npmjs.com/misc/semver#versions
pragma solidity 0.4.0;
pragma solidity ^0.4.0;
pragma experimental ABIEncoderV2;

import "SomeFile.sol";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`AllSolidityFeatures.sol format 1`] = `
exports[`AllSolidityFeatures.sol - {"compiler":"0.4.26"} format 1`] = `
====================================options=====================================
compiler: "0.4.26"
parsers: ["slang"]
printWidth: 80
| printWidth
Expand Down
2 changes: 1 addition & 1 deletion tests/format/AllSolidityFeaturesV0.4.26/format.test.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
runFormatTest(import.meta, ['slang']);
runFormatTest(import.meta, ['slang'], { compiler: '0.4.26' });
5 changes: 3 additions & 2 deletions tests/format/BasicIterator/__snapshots__/format.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`BasicIterator.sol format 1`] = `
exports[`BasicIterator.sol - {"compiler":"0.4.26"} format 1`] = `
====================================options=====================================
compiler: "0.4.26"
parsers: ["slang"]
printWidth: 80
| printWidth
Expand Down
2 changes: 1 addition & 1 deletion tests/format/BasicIterator/format.test.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
runFormatTest(import.meta, ['slang']);
runFormatTest(import.meta, ['slang'], { compiler: '0.4.26' });
3 changes: 0 additions & 3 deletions tests/format/Comments/Comments.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
pragma solidity ^0.4.29;


contract Comments1 {
/* solhint-disable var-name-mixedcase */
IEIP712DomainSeparator private EIP712domainSeparator;
Expand Down
5 changes: 0 additions & 5 deletions tests/format/Comments/__snapshots__/format.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ parsers: ["slang"]
printWidth: 80
| printWidth
=====================================input======================================
pragma solidity ^0.4.29;


contract Comments1 {
/* solhint-disable var-name-mixedcase */
IEIP712DomainSeparator private EIP712domainSeparator;
Expand Down Expand Up @@ -178,8 +175,6 @@ contract Comments13 {
}
}
=====================================output=====================================
pragma solidity ^0.4.29;

contract Comments1 {
/* solhint-disable var-name-mixedcase */
IEIP712DomainSeparator private EIP712domainSeparator;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`FunctionDefinitions.sol format 1`] = `
exports[`FunctionDefinitions.sol - {"compiler":"0.5.17"} format 1`] = `
====================================options=====================================
compiler: "0.5.17"
parsers: ["slang"]
printWidth: 80
| printWidth
Expand Down
2 changes: 1 addition & 1 deletion tests/format/FunctionDefinitionsV0.5.0/format.test.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
runFormatTest(import.meta, ['slang']);
runFormatTest(import.meta, ['slang'], { compiler: '0.5.17' });
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`FunctionDefinitions.sol format 1`] = `
exports[`FunctionDefinitions.sol - {"compiler":"0.5.17"} format 1`] = `
====================================options=====================================
compiler: "0.5.17"
parsers: ["slang"]
printWidth: 80
| printWidth
Expand Down
2 changes: 1 addition & 1 deletion tests/format/FunctionDefinitionsv0.5.0/format.test.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
runFormatTest(import.meta, ['slang']);
runFormatTest(import.meta, ['slang'], { compiler: '0.5.17' });
5 changes: 3 additions & 2 deletions tests/format/IndexOf/__snapshots__/format.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`IndexOf.sol format 1`] = `
exports[`IndexOf.sol - {"compiler":"0.4.26"} format 1`] = `
====================================options=====================================
compiler: "0.4.26"
parsers: ["slang"]
printWidth: 80
| printWidth
Expand Down
2 changes: 1 addition & 1 deletion tests/format/IndexOf/format.test.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
runFormatTest(import.meta, ['slang']);
runFormatTest(import.meta, ['slang'], { compiler: '0.4.26' });
5 changes: 3 additions & 2 deletions tests/format/MemberAccess/__snapshots__/format.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`MemberAccess.sol format 1`] = `
exports[`MemberAccess.sol - {"compiler":"0.6.12"} format 1`] = `
====================================options=====================================
compiler: "0.6.12"
parsers: ["slang"]
printWidth: 80
| printWidth
Expand Down
2 changes: 1 addition & 1 deletion tests/format/MemberAccess/format.test.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
runFormatTest(import.meta, ['slang']);
runFormatTest(import.meta, ['slang'], { compiler: '0.6.12' });
Loading