Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
108 changes: 45 additions & 63 deletions package-lock.json

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

8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,8 @@
"@babel/code-frame": "^7.26.2",
"@types/jest": "^29.5.14",
"@types/semver": "^7.5.8",
"@types/strip-comments": "^2.0.4",
"@typescript-eslint/eslint-plugin": "^8.12.2",
"@typescript-eslint/parser": "^8.12.2",
"@typescript-eslint/eslint-plugin": "^8.14.0",
"@typescript-eslint/parser": "^8.14.0",
"c8": "^9.1.0",
"cross-env": "^7.0.3",
"eslint": "^9.14.0",
Expand All @@ -111,8 +110,7 @@
"dependencies": {
"@nomicfoundation/slang": "0.18.3",
"@solidity-parser/parser": "^0.18.0",
"semver": "^7.6.3",
"strip-comments": "^2.0.1"
"semver": "^7.6.3"
},
"peerDependencies": {
"prettier": ">=3.0.0"
Expand Down
66 changes: 53 additions & 13 deletions src/slang-utils/create-parser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { VersionExpressionSets as SlangVersionExpressionSets } from '@nomicfoundation/slang/ast';
import { NonterminalKind, Query } from '@nomicfoundation/slang/cst';
import { Parser } from '@nomicfoundation/slang/parser';
import strip from 'strip-comments';
import prettier from 'prettier';
import {
maxSatisfying,
minSatisfying,
Expand All @@ -9,6 +10,12 @@ import {
satisfies,
validRange
} from 'semver';
import slangPrint from '../slangPrinter.js';
import { locEnd, locStart } from './loc.js';
import { VersionExpressionSets } from '../slang-nodes/VersionExpressionSets.js';

import type { NonterminalNode } from '@nomicfoundation/slang/cst';
import type { Parser as PrettierParser } from 'prettier';

const supportedVersions = Parser.supportedVersions();

Expand All @@ -25,15 +32,48 @@ const milestoneVersions = Array.from(
return versions;
}, []);

const bypassParse =
(parseOutput: NonterminalNode) => (): VersionExpressionSets => {
// We don't need to parse the text twice if we already have the
// NonterminalNode.
const parsed = new VersionExpressionSets(
new SlangVersionExpressionSets(parseOutput),
0
);
parsed.comments = [];
return parsed;
};

const options = {
plugins: [
{
languages: [{ name: 'SolidityPragma', parsers: ['slangPragma'] }],
parsers: {
slangPragma: {
astFormat: 'slang-ast',
locStart,
locEnd
} as PrettierParser
},
printers: { ['slang-ast']: { print: slangPrint } }
}
],
parser: 'slangPragma'
};

const query = Query.parse(
Copy link
Member

Choose a reason for hiding this comment

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

I noticed you moved query out of the tryToCollectPragmas function, but you only use it there; should we move it back to its original location?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this way the query is computed only once and not every time it tryToCollectPragmas is called

'[VersionPragma @versionRanges [VersionExpressionSets]]'
);

// TODO if we ended up selecting the same version that the pragmas were parsed with,
// should we be able to reuse/just return the already parsed CST, instead of
// returning a Parser and forcing user to parse it again?
export function createParser(text: string): Parser {
export async function createParser(text: string): Promise<Parser> {
let inferredRanges: string[] = [];

for (const version of milestoneVersions) {
try {
inferredRanges = tryToCollectPragmas(text, version);
inferredRanges = await tryToCollectPragmas(text, version);
break;
} catch {}
}
Expand All @@ -57,22 +97,22 @@ export function createParser(text: string): Parser {
: Parser.create(supportedVersions[supportedVersions.length - 1]);
}

function tryToCollectPragmas(text: string, version: string): string[] {
const language = Parser.create(version);
const parseOutput = language.parse(NonterminalKind.SourceUnit, text);
const query = Query.parse(
'[VersionPragma @versionRanges [VersionExpressionSets]]'
);
async function tryToCollectPragmas(
text: string,
version: string
): Promise<string[]> {
const parser = Parser.create(version);
const parseOutput = parser.parse(NonterminalKind.SourceUnit, text);

const matches = parseOutput.createTreeCursor().query([query]);
const ranges: string[] = [];

let match;
while ((match = matches.next())) {
ranges.push(
strip(match.captures.versionRanges[0].node.unparse(), {
keepProtected: true
})
options.plugins[0].parsers.slangPragma.parse = bypassParse(
match.captures.versionRanges[0].node.asNonterminalNode()!
);
ranges.push(await prettier.format(text, options));
}

if (ranges.length === 0) {
Expand Down
6 changes: 3 additions & 3 deletions src/slangSolidityParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ import type { AstNode } from './slang-nodes/types.d.ts';

const supportedVersions = Parser.supportedVersions();

export default function parse(
export default async function parse(
text: string,
options: ParserOptions<AstNode>
): AstNode {
): Promise<AstNode> {
const compiler = maxSatisfying(supportedVersions, options.compiler);

const parser =
compiler && supportedVersions.includes(compiler)
? Parser.create(compiler)
: createParser(text);
: await createParser(text);

const parseOutput = parser.parse(NonterminalKind.SourceUnit, text);
printWarning(
Expand Down
20 changes: 15 additions & 5 deletions tests/unit/slang-utils/create-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,31 @@ describe('inferLanguage', function () {
{
description:
'should use the latest version if the range is outside the supported versions',
source: `pragma solidity ^0.8.27;`,
source: `pragma solidity ^0.9.27;`,
version: latestSupportedVersion
},
{
description: 'broken by new lines, whitespaces, and comments',
source: `pragma solidity 0.
// comment 1
7.
/* comment 2*/
3;`,
version: '0.7.3'
}
];

for (const { description, source, version } of fixtures) {
test(description, function () {
const parser = createParser(source);
test(description, async function () {
const parser = await createParser(source);
expect(parser.version).toEqual(version);
});
}

test.skip('should throw an error if there are incompatible ranges', function () {
expect(() =>
createParser(`pragma solidity ^0.8.0; pragma solidity 0.7.6;`)
expect(
async () =>
await createParser(`pragma solidity ^0.8.0; pragma solidity 0.7.6;`)
).toThrow();
});
});