diff --git a/src/slang-utils/create-parser.ts b/src/slang-utils/create-parser.ts index f7405c449..3bdb6814c 100644 --- a/src/slang-utils/create-parser.ts +++ b/src/slang-utils/create-parser.ts @@ -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 -): { 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); } diff --git a/src/slangSolidityParser.ts b/src/slangSolidityParser.ts index 8963d6252..9381fa24f 100644 --- a/src/slangSolidityParser.ts +++ b/src/slangSolidityParser.ts @@ -11,10 +11,10 @@ export default function parse( text: string, options: ParserOptions ): 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()), diff --git a/tests/config/test-antlr-format.js b/tests/config/test-antlr-format.js index a55df8d43..a40e433b2 100644 --- a/tests/config/test-antlr-format.js +++ b/tests/config/test-antlr-format.js @@ -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(), }); diff --git a/tests/unit/slang-utils/create-parser.test.js b/tests/unit/slang-utils/create-parser.test.js index 57cba8a38..7ac335206 100644 --- a/tests/unit/slang-utils/create-parser.test.js +++ b/tests/unit/slang-utils/create-parser.test.js @@ -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 () {