Skip to content
Open
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
68 changes: 49 additions & 19 deletions src/slang-utils/create-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,83 @@ import type { ParserOptions } from 'prettier';
import type { PrintableNode } from '../slang-nodes/types.d.ts';

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

function parserAndOutput(
text: string,
// Most files allow the latest version, so we parse with it first and only
// parse again if their pragmas ask for an older one.
const latestParser = Parser.create(latestSupportedVersion);

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

): { version: string; parseOutput: ParseOutput } {
if (!parseOutput.isValid())
throw new Error(
`We encountered the following syntax error:\n\n\t${parseOutput.errors()[0].message}\n\n${reason}`
);

return { parser, parseOutput };
return { version, parseOutput };
}

function versionAndOutput(
text: string,
version: string,
reason: string
): { version: string; parseOutput: ParseOutput } {
return validated(
version,
Parser.create(version).parseNonterminal(NonterminalKind.SourceUnit, text),
reason
);
}

// The text of every pragma directive in the tree. Slang's version inference
// only looks at pragmas, but given the whole file it analyzes all of it, which
// costs about as much as parsing it.
function pragmasOf(parseOutput: ParseOutput): string {
const cursor = parseOutput.createTreeCursor();
let pragmas = '';
while (cursor.goToNextNonterminalWithKind(NonterminalKind.PragmaDirective)) {
pragmas += `${cursor.node.unparse()}\n`;
}
return pragmas;
}

export function createParser(
text: string,
options: ParserOptions<PrintableNode>
): { parser: Parser; parseOutput: ParseOutput } {
): { version: string; parseOutput: ParseOutput } {
const compiler = maxSatisfying(supportedVersions, options.compiler);
if (compiler) {
return parserAndOutput(
return versionAndOutput(
text,
compiler,
`Based on the compiler option provided, we inferred your code to be using Solidity version ${compiler}. If you would like to change that, specify a different version in your \`.prettierrc\` file.`
);
}

const inferredRanges = LanguageFacts.inferLanguageVersions(text);
const latestOutput = latestParser.parseNonterminal(
NonterminalKind.SourceUnit,
text
);
const inferredRanges = LanguageFacts.inferLanguageVersions(
pragmasOf(latestOutput)
);
const inferredLength = inferredRanges.length;

if (inferredLength === 0 || inferredLength === supportedLength) {
return parserAndOutput(
text,
if (inferredLength === 0 || inferredLength === supportedVersions.length) {
return validated(
latestSupportedVersion,
latestOutput,
`We couldn't infer a Solidity version based on the pragma statements in your code so we defaulted to ${latestSupportedVersion}. You might be attempting to use a syntax not yet supported by Slang or you might want to specify a version in your \`.prettierrc\` file.`
);
}

const inferredVersion = inferredRanges[inferredLength - 1];
return parserAndOutput(
text,
inferredVersion,
`Based on the pragma statements, we inferred your code to be using Solidity version ${inferredVersion}. If you would like to change that, update the pragmas in your source file, or specify a version in your \`.prettierrc\` file.`
);
const reason = `Based on the pragma statements, we inferred your code to be using Solidity version ${inferredVersion}. If you would like to change that, update the pragmas in your source file, or specify a version in your \`.prettierrc\` file.`;

return inferredVersion === latestSupportedVersion
? validated(latestSupportedVersion, latestOutput, reason)
: versionAndOutput(text, inferredVersion, reason);
}
4 changes: 2 additions & 2 deletions src/slangSolidityParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export default function parse(
text: string,
options: ParserOptions<PrintableNode>
): PrintableNode {
const { parser, parseOutput } = createParser(text, options);
const { version, parseOutput } = createParser(text, options);

// We update the compiler version with the inferred one.
options.compiler = parser.languageVersion;
options.compiler = version;
const comments: Comment[] = [];
const parsed = new SourceUnit(
new SlangSourceUnit(parseOutput.tree.asNonterminalNode()),
Expand Down
4 changes: 2 additions & 2 deletions tests/config/test-antlr-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ function testAntlrFormat(testCase, name) {

// Compare with ANTLR's format
const createParser = await getCreateParser();
const { parser } = createParser(code, formatOptions);
const { version } = createParser(code, formatOptions);
const prettier = await getPrettier();
const { formatted: antlrOutput } = await prettier.formatWithCursor(code, {
...formatOptions,
// Since Slang forces us to decide on a compiler version, we need to do the
// same for ANTLR unless it was already given as an option.
compiler: formatOptions.compiler || parser.languageVersion,
compiler: formatOptions.compiler || version,
parser: "antlr",
plugins: await getPlugins(),
});
Expand Down
27 changes: 15 additions & 12 deletions tests/unit/slang-utils/create-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,32 +82,35 @@ describe('inferLanguage', function () {

for (const { description, source, version, skip } of fixtures) {
(skip ? test.skip : test)(description, function () {
const { parser } = createParser(source, options);
expect(parser.languageVersion).toEqual(version);
const { version: inferredVersion } = createParser(source, options);
expect(inferredVersion).toEqual(version);
});
}

test('should use the latest successful version if the source has no pragmas', function () {
let { parser } = createParser(`contract Foo {}`, options);
expect(parser.languageVersion).toEqual(latestSupportedVersion);
let { version: inferredVersion } = createParser(`contract Foo {}`, options);
expect(inferredVersion).toEqual(latestSupportedVersion);

// ({ parser } = createParser(`contract Foo {byte bar;}`, options));
// expect(parser.languageVersion).toEqual('0.7.6');
// ({ version } = createParser(`contract Foo {byte bar;}`, options));
// expect(version).toEqual('0.7.6');
});

test('should use compiler option if given', function () {
let { parser } = createParser(`pragma solidity ^0.8.0;`, {
let { version: inferredVersion } = createParser(`pragma solidity ^0.8.0;`, {
compiler: '0.8.20'
});
expect(parser.languageVersion).toEqual('0.8.20');
expect(inferredVersion).toEqual('0.8.20');

({ parser } = createParser(`pragma solidity ^0.8.0;`, {
({ version: inferredVersion } = createParser(`pragma solidity ^0.8.0;`, {
compiler: '0.8.2'
}));
expect(parser.languageVersion).toEqual('0.8.2');
expect(inferredVersion).toEqual('0.8.2');

({ parser } = createParser(`pragma solidity ^0.7.0;`, {}));
expect(parser.languageVersion).toEqual('0.7.6');
({ version: inferredVersion } = createParser(
`pragma solidity ^0.7.0;`,
{}
));
expect(inferredVersion).toEqual('0.7.6');
});

test('should throw if compiler option does not match the syntax', function () {
Expand Down
Loading