Skip to content

Commit

Permalink
avoid parsing an extra time if the last parsed version matches the in…
Browse files Browse the repository at this point in the history
…ferred version
  • Loading branch information
Janther committed Dec 4, 2024
1 parent a15ae47 commit cdfa89c
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 37 deletions.
65 changes: 48 additions & 17 deletions src/slang-utils/create-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
} 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 = Parser.supportedVersions();

Expand All @@ -32,17 +34,36 @@ const query = Query.parse(
'[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, options: ParserOptions): Parser {
export function createParser(
text: string,
options: ParserOptions<AstNode>
): [Parser, ParseOutput] {
const compiler = maxSatisfying(supportedVersions, options.compiler);
let parser: Parser;
if (compiler) {
parser = Parser.create(compiler);
return [parser, parser.parse(NonterminalKind.SourceUnit, text)];
}

parser = Parser.create(milestoneVersions[0]);
let parseOutput;
let inferredRanges: string[] = [];

for (const version of milestoneVersions) {
try {
inferredRanges = tryToCollectPragmas(text, version);
break;
} catch {}
try {
parseOutput = parser.parse(NonterminalKind.SourceUnit, text);
inferredRanges = tryToCollectPragmas(parseOutput, parser);
} catch {
for (let i = 1; i <= milestoneVersions.length; i += 1) {
try {
const version = milestoneVersions[i];
parser = Parser.create(version);
parseOutput = parser.parse(NonterminalKind.SourceUnit, text);
inferredRanges = tryToCollectPragmas(parseOutput, parser);
break;
} catch {
continue;
}
}
}

const satisfyingVersions = inferredRanges.reduce(
Expand All @@ -59,14 +80,24 @@ export function createParser(text: string, options: ParserOptions): Parser {
supportedVersions
);

return satisfyingVersions.length > 0
? Parser.create(satisfyingVersions[satisfyingVersions.length - 1])
: Parser.create(supportedVersions[supportedVersions.length - 1]);
const inferredVersion =
satisfyingVersions.length > 0
? satisfyingVersions[satisfyingVersions.length - 1]
: supportedVersions[supportedVersions.length - 1];

if (inferredVersion !== parser.version) {
parser = Parser.create(inferredVersion);
parseOutput = parser.parse(NonterminalKind.SourceUnit, text);
}

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return [parser, parseOutput!];
}

function tryToCollectPragmas(text: string, version: string): string[] {
const language = Parser.create(version);
const parseOutput = language.parse(NonterminalKind.SourceUnit, text);
function tryToCollectPragmas(
parseOutput: ParseOutput,
parser: Parser
): string[] {
const matches = parseOutput.createTreeCursor().query([query]);
const ranges: string[] = [];

Expand All @@ -87,11 +118,11 @@ function tryToCollectPragmas(text: string, version: string): string[] {
if (ranges.length === 0) {
// If we didn't find pragmas but succeeded parsing the source we keep it.
if (parseOutput.isValid()) {
return [version];
return [parser.version];
}
// Otherwise we throw.
throw new Error(
`Using version ${version} did not find any pragma statement and does not parse without errors.`
`Using version ${parser.version} did not find any pragma statement and does not parse without errors.`
);
}

Expand Down
20 changes: 1 addition & 19 deletions src/slangSolidityParser.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,17 @@
// https://prettier.io/docs/en/plugins.html#parsers
import { Parser } from '@nomicfoundation/slang/parser';
import { NonterminalKind } from '@nomicfoundation/slang/cst';
import { SourceUnit as SlangSourceUnit } from '@nomicfoundation/slang/ast';
import { maxSatisfying } from 'semver';
import { clearOffsets } from './slang-utils/metadata.js';
import { createParser } from './slang-utils/create-parser.js';
import { printWarning } from './slang-utils/print-warning.js';
import { SourceUnit } from './slang-nodes/SourceUnit.js';

import type { ParserOptions } from 'prettier';
import type { AstNode } from './slang-nodes/types.d.ts';

const supportedVersions = Parser.supportedVersions();

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

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

const parseOutput = parser.parse(NonterminalKind.SourceUnit, text);
printWarning(
compiler
? `Using version ${parser.version} based on the compiler option provided.`
: `Inferred version ${parser.version} based on the pragma statements in the code.`
);
const [parser, parseOutput] = createParser(text, options);

if (parseOutput.isValid()) {
// We update the compiler version by the inferred one.
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/slang-utils/create-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('inferLanguage', function () {

for (const { description, source, version } of fixtures) {
test(description, function () {
const parser = createParser(source, options);
const [parser] = createParser(source, options);
expect(parser.version).toEqual(version);
});
}
Expand Down

0 comments on commit cdfa89c

Please sign in to comment.