prettier-plugin-solidity
Version:
A Prettier Plugin for automatically formatting your Solidity code.
146 lines (128 loc) • 4.39 kB
text/typescript
import { VersionExpressionSets as SlangVersionExpressionSets } from '@nomicfoundation/slang/ast';
import { NonterminalKind, Query } 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 type { ParseOutput } from '@nomicfoundation/slang/parser';
import type { ParserOptions } from 'prettier';
import type { AstNode } from '../slang-nodes/types.js';
const supportedVersions = LanguageFacts.allVersions();
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;
export function createParser(
text: string,
options: ParserOptions<AstNode>
): [Parser, ParseOutput] {
const compiler = maxSatisfying(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)
);
},
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);
}
// 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()
)
);
}
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.
throw new Error(
`Using version ${parser.languageVersion} did not find any pragma statement and does not parse without errors.`
);
}
return ranges;
}