diff --git a/README.md b/README.md index fbcf564b3..2235ad21f 100644 --- a/README.md +++ b/README.md @@ -109,15 +109,26 @@ The following is the default configuration internally used by this plugin. "plugins": ["prettier-plugin-solidity"], "overrides": [ { - "files": "*.sol", + "files": ["*.sol", "*.yul"], "options": { - "parser": "slang", "printWidth": 80, "tabWidth": 4, "useTabs": false, "singleQuote": false, "bracketSpacing": false, } + }, + { + "files": "*.sol", + "options": { + "parser": "slang" + } + }, + { + "files": "*.yul", + "options": { + "parser": "slang-yul" + } } ] } diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..56444122d --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,5 @@ +export const slangParserId = 'slang'; +export const slangYulParserId = 'slang-yul'; +export const antlrParserId = 'antlr'; +export const slangAstId = 'slang-ast'; +export const antlrAstId = 'antlr-ast'; diff --git a/src/index.ts b/src/index.ts index b5efe7d8b..04f58c9a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,9 +6,17 @@ import options from './options.js'; import antlrParse from './parser.js'; import antlrPrint from './printer.js'; import slangParse from './slangSolidityParser.js'; +import yulParse from './slangYulParser.js'; import slangPrint from './slangPrinter.js'; import { isBlockComment, isComment } from './slang-utils/is-comment.js'; import { locEnd, locStart } from './slang-utils/loc.js'; +import { + antlrAstId, + antlrParserId, + slangAstId, + slangParserId, + slangYulParserId +} from './constants.js'; import type { Parser, @@ -18,11 +26,6 @@ import type { } from 'prettier'; import type { AstNode } from './slang-nodes/types.d.ts'; -const slangParserId = 'slang'; -const antlrParserId = 'antlr'; -const slangAstId = 'slang-ast'; -const antlrAstId = 'antlr-ast'; - // https://prettier.io/docs/en/plugins.html#languages // https://github.com/github-linguist/linguist/blob/master/lib/linguist/languages.yml const languages: SupportLanguage[] = [ @@ -34,6 +37,15 @@ const languages: SupportLanguage[] = [ extensions: ['.sol'], parsers: [slangParserId, antlrParserId], vscodeLanguageIds: ['solidity'] + }, + { + linguistLanguageId: 237469033, + name: 'Yul', + aceMode: 'text', + tmScope: 'source.yul', + extensions: ['.yul'], + parsers: [slangYulParserId], + vscodeLanguageIds: ['yul'] } ]; @@ -45,9 +57,16 @@ const slangParser: Parser = { locStart, locEnd }; +const yulParser: Parser = { + astFormat: slangAstId, + parse: yulParse, + locStart, + locEnd +}; const parsers = { [slangParserId]: slangParser, + [slangYulParserId]: yulParser, [antlrParserId]: antlrParser }; diff --git a/src/slang-utils/create-parser.ts b/src/slang-utils/create-parser.ts index f5cd9ffc6..beeb6b5fa 100644 --- a/src/slang-utils/create-parser.ts +++ b/src/slang-utils/create-parser.ts @@ -2,6 +2,7 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { Parser } from '@nomicfoundation/slang/parser'; import { LanguageFacts } from '@nomicfoundation/slang/utils'; import { maxSatisfying } from 'semver'; +import { slangParserId, slangYulParserId } from '../constants.js'; import type { ParseOutput } from '@nomicfoundation/slang/parser'; import type { ParserOptions } from 'prettier'; @@ -9,16 +10,26 @@ import type { AstNode } from '../slang-nodes/types.d.ts'; const supportedVersions = LanguageFacts.allVersions(); const supportedLength = supportedVersions.length; +const rootKindMap = new Map['parser'], NonterminalKind>([ + [slangParserId, NonterminalKind.SourceUnit], + [slangYulParserId, NonterminalKind.YulBlock] +]); function parserAndOutput( text: string, - version: string + version: string, + { parser: optionsParser }: ParserOptions ): { parser: Parser; parseOutput: ParseOutput } { + const rootKind = rootKindMap.get(optionsParser); + + if (rootKind === undefined) { + throw new Error( + `Parser '${optionsParser as string}' is not supported for Language Inference.` + ); + } + const parser = Parser.create(version); - return { - parser, - parseOutput: parser.parseNonterminal(NonterminalKind.SourceUnit, text) - }; + return { parser, parseOutput: parser.parseNonterminal(rootKind, text) }; } function createError( @@ -36,7 +47,7 @@ export function createParser( ): { parser: Parser; parseOutput: ParseOutput } { const compiler = maxSatisfying(supportedVersions, options.compiler); if (compiler) { - const result = parserAndOutput(text, compiler); + const result = parserAndOutput(text, compiler, options); if (!result.parseOutput.isValid()) throw createError( @@ -55,7 +66,8 @@ export function createParser( if (inferredLength === 0 || inferredLength === supportedLength) { const result = parserAndOutput( text, - supportedVersions[supportedLength - 1] + supportedVersions[supportedLength - 1], + options ); if (!result.parseOutput.isValid()) @@ -70,7 +82,8 @@ export function createParser( const result = parserAndOutput( text, - inferredRanges[inferredRanges.length - 1] + inferredRanges[inferredLength - 1], + options ); if (!result.parseOutput.isValid()) diff --git a/src/slangYulParser.ts b/src/slangYulParser.ts new file mode 100644 index 000000000..5020cbe7a --- /dev/null +++ b/src/slangYulParser.ts @@ -0,0 +1,28 @@ +// https://prettier.io/docs/en/plugins.html#parsers +import { YulBlock as SlangYulBlock } from '@nomicfoundation/slang/ast'; +import { createParser } from './slang-utils/create-parser.js'; +import { YulBlock } from './slang-nodes/YulBlock.js'; + +import type { ParserOptions } from 'prettier'; +import type { AstNode, Comment } from './slang-nodes/types.d.ts'; + +export default function parse( + text: string, + options: ParserOptions +): AstNode { + const { parser, parseOutput } = createParser(text, options); + + // We update the compiler version by the inferred one. + options.compiler = parser.languageVersion; + const comments: Comment[] = []; + const parsed = new YulBlock( + new SlangYulBlock(parseOutput.tree.asNonterminalNode()), + { offsets: new Map(), comments }, + options + ); + + // Because of comments being extracted like a Russian doll, the order needs + // to be fixed at the end. + parsed.comments = comments.sort((a, b) => a.loc.start - b.loc.start); + return parsed; +} diff --git a/tests/format/Yul/__snapshots__/format.test.js.snap b/tests/format/Yul/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..4b7434cc2 --- /dev/null +++ b/tests/format/Yul/__snapshots__/format.test.js.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`example1.yul format 1`] = ` +====================================options===================================== +parsers: ["slang-yul"] +printWidth: 80 + | printWidth +=====================================input====================================== +{ + function power(base, exponent) -> result + { + switch exponent + case 0 { result := 1 } + case 1 { result := base } + default + { + result := power(mul(base, base), div(exponent, 2)) + switch mod(exponent, 2) + case 1 { result := mul(base, result) } + } + } +} +=====================================output===================================== +{ + function power(base, exponent) -> result { + switch exponent + case 0 { + result := 1 + } + case 1 { + result := base + } + default { + result := power(mul(base, base), div(exponent, 2)) + switch mod(exponent, 2) + case 1 { + result := mul(base, result) + } + } + } +} +================================================================================ +`; + +exports[`example2.yul format 1`] = ` +====================================options===================================== +parsers: ["slang-yul"] +printWidth: 80 + | printWidth +=====================================input====================================== +{ + function power(base, exponent) -> result + { + result := 1 for { let i := 0 } lt(i, exponent) { i := add(i, 1) } + { + result := mul(result, base) + } + } +} +=====================================output===================================== +{ + function power(base, exponent) -> result { + result := 1 + for { + let i := 0 + } lt(i, exponent) { + i := add(i, 1) + } { + result := mul(result, base) + } + } +} +================================================================================ +`; diff --git a/tests/format/Yul/example1.yul b/tests/format/Yul/example1.yul new file mode 100644 index 000000000..4491a54b6 --- /dev/null +++ b/tests/format/Yul/example1.yul @@ -0,0 +1,14 @@ +{ + function power(base, exponent) -> result + { + switch exponent + case 0 { result := 1 } + case 1 { result := base } + default + { + result := power(mul(base, base), div(exponent, 2)) + switch mod(exponent, 2) + case 1 { result := mul(base, result) } + } + } +} \ No newline at end of file diff --git a/tests/format/Yul/example2.yul b/tests/format/Yul/example2.yul new file mode 100644 index 000000000..0510ef31c --- /dev/null +++ b/tests/format/Yul/example2.yul @@ -0,0 +1,9 @@ +{ + function power(base, exponent) -> result + { + result := 1 for { let i := 0 } lt(i, exponent) { i := add(i, 1) } + { + result := mul(result, base) + } + } +} \ No newline at end of file diff --git a/tests/format/Yul/format.test.js b/tests/format/Yul/format.test.js new file mode 100644 index 000000000..209d90b96 --- /dev/null +++ b/tests/format/Yul/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ['slang-yul']); diff --git a/tests/unit/slang-utils/create-parser.test.js b/tests/unit/slang-utils/create-parser.test.js index cc28ce06a..78d15d125 100644 --- a/tests/unit/slang-utils/create-parser.test.js +++ b/tests/unit/slang-utils/create-parser.test.js @@ -1,9 +1,10 @@ import { LanguageFacts } from '@nomicfoundation/slang/utils'; import { createParser } from '../../../src/slang-utils/create-parser.js'; +import { slangParserId } from '../../../src/constants.js'; describe('inferLanguage', function () { const latestSupportedVersion = LanguageFacts.latestVersion(); - const options = { filepath: 'test.sol' }; + const options = { parser: slangParserId }; const fixtures = [ { @@ -97,22 +98,27 @@ describe('inferLanguage', function () { test('should use compiler option if given', function () { let { parser } = createParser(`pragma solidity ^0.8.0;`, { + ...options, compiler: '0.8.20' }); expect(parser.languageVersion).toEqual('0.8.20'); ({ parser } = createParser(`pragma solidity ^0.8.0;`, { + ...options, compiler: '0.8.2' })); expect(parser.languageVersion).toEqual('0.8.2'); - ({ parser } = createParser(`pragma solidity ^0.7.0;`, {})); + ({ parser } = createParser(`pragma solidity ^0.7.0;`, options)); expect(parser.languageVersion).toEqual('0.7.6'); }); test('should throw if compiler option does not match the syntax', function () { expect(() => - createParser(`contract Foo {byte bar;}`, { compiler: '0.8.0' }) + createParser(`contract Foo {byte bar;}`, { + ...options, + compiler: '0.8.0' + }) ).toThrow( 'Based on the compiler option provided, we inferred your code to be using Solidity version' );