Skip to content

Commit

Permalink
Scope rework (#124)
Browse files Browse the repository at this point in the history
* initial commit

* wip on scope2

* big WIP

* first test up and failing as expected

* working towards imp filter fn

* might remove scopeUtils

* impl EachExpression almost done

* one failing test

* fixed scope propagation

* impl FunctionExpression

* impl Record

* WIP on Section impl

* fixing bugs, next is LetExpression

* partial impl for LetExpression

* finished LetExpression impl

* working on updating to v2 inspection

* working on scope/inspection v2

* it builds and tests pass, still needs refactoring

* removed dead code

* lots of minor changes

* prettier format

* rename

* more renaming, updated tryInspection

* updated tryLexParseInspect

* created top level Task export

* updated inspection example

* updated task usage example

* updated readme link
  • Loading branch information
JordanBoltonMN authored Apr 8, 2020
1 parent 9d3de17 commit 2745e67
Show file tree
Hide file tree
Showing 35 changed files with 2,091 additions and 2,378 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A parser for the [Power Query/M](https://docs.microsoft.com/en-us/power-query/)

## How to use

The most common way to consume the project is to interact with the helper functions found in [src/tasks.ts](src/tasks.ts). There are all-in-one functions, such as `tryLexParseInspection`, which does a full pass on a given document. There are also incremental functions, such as `tryLex` and `tryParse`, which perform one step at a time. Minimal code samples can be found in [example.ts](src/example.ts).
The most common way to consume the project is to interact with the helper functions found in [src/task.ts](src/task.ts). There are all-in-one functions, such as `tryLexParseInspection`, which does a full pass on a given document. There are also incremental functions, such as `tryLex` and `tryParse`, which perform one step at a time. Minimal code samples can be found in [example.ts](src/example.ts).

## Things to note

Expand Down
23 changes: 17 additions & 6 deletions src/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@

/* tslint:disable:no-console */

import { Inspection } from ".";
import { Task } from ".";
import { ResultUtils } from "./common";
import { Lexer, LexError, LexerSnapshot, TriedLexerSnapshot } from "./lexer";
import { ParseError } from "./parser";
import { DefaultSettings } from "./settings";
import { TriedLexParse, TriedLexParseInspection, tryLexParse, tryLexParseInspection } from "./tasks";

parseText(`if true then 1 else 2`);

// @ts-ignore
function parseText(text: string): void {
// Try lexing and parsing the argument which returns a Result object.
// A Result<T, E> is the union (Ok<T> | Err<E>).
const triedLexParse: TriedLexParse = tryLexParse(DefaultSettings, text);
const triedLexParse: Task.TriedLexParse = Task.tryLexParse(DefaultSettings, text);

// If the Result is an Ok, then dump the jsonified abstract syntax tree (AST) which was parsed.
if (ResultUtils.isOk(triedLexParse)) {
Expand Down Expand Up @@ -102,12 +101,24 @@ function lexText(text: string): void {
function inspectText(text: string, position: Inspection.Position): void {
// Having a LexError thrown will abort the inspection and return the offending LexError.
// So long as a TriedParse is created from reaching the parsing stage then an inspection will be returned.
const triedInspection: TriedLexParseInspection = tryLexParseInspection(DefaultSettings, text, position);
const triedInspection: Task.TriedLexParseInspect = Task.tryLexParseInspection(DefaultSettings, text, position);
if (ResultUtils.isErr(triedInspection)) {
console.log(`Inspection failed due to: ${triedInspection.error.message}`);
return;
}
const inspected: Inspection.Inspected = triedInspection.value;
const inspection: Task.InspectionOk = triedInspection.value;

console.log(`Inspected scope: ${[...inspected.scope.entries()]}`);
for (const identifier of inspection.scope.keys()) {
console.log(`Identifier: ${identifier} has type ${inspection.scopeType.get(identifier)!.kind}`);
}

console.log(`Suggested keyword autocomplete: ${inspection.autocomplete.join(", ")}`);

console.log(`InvokeExpression name: ${inspection.maybeInvokeExpression?.maybeName}`);
console.log(
`InvokeExpression number of arguments: ${inspection.maybeInvokeExpression?.maybeArguments?.numArguments}`,
);
console.log(
`InvokeExpression argument position: ${inspection.maybeInvokeExpression?.maybeArguments?.positionArgumentIndex}`,
);
}
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
// Licensed under the MIT license.

import * as Inspection from "./inspection";
import * as Task from "./task";

export { Inspection };
export { Inspection, Task };
export * from "./common";
export * from "./lexer";
export * from "./localization";
export * from "./parser";
export * from "./settings";
export * from "./tasks";
export * from "./type";
78 changes: 0 additions & 78 deletions src/inspection/activeNode/activeNodeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { CommonError } from "../../common";
import {
Ast,
AstUtils,
Expand Down Expand Up @@ -64,83 +63,6 @@ export function maybeActiveNode(
};
}

export function expectRoot(activeNode: ActiveNode): TXorNode {
const ancestry: ReadonlyArray<TXorNode> = activeNode.ancestry;
return ancestry[ancestry.length - 1];
}

export function expectLeaf(activeNode: ActiveNode): TXorNode {
return activeNode.ancestry[0];
}

export function maybePreviousXorNode(
activeNode: ActiveNode,
ancestorIndex: number,
n: number = 1,
maybeNodeKinds: ReadonlyArray<Ast.NodeKind> | undefined = undefined,
): TXorNode | undefined {
const maybeXorNode: TXorNode | undefined = activeNode.ancestry[ancestorIndex - n];
if (maybeXorNode !== undefined && maybeNodeKinds !== undefined) {
return maybeNodeKinds.indexOf(maybeXorNode.node.kind) !== -1 ? maybeXorNode : undefined;
} else {
return maybeXorNode;
}
}

export function maybeNextXorNode(activeNode: ActiveNode, ancestorIndex: number, n: number = 1): TXorNode | undefined {
return activeNode.ancestry[ancestorIndex + n];
}

export function expectPreviousXorNode(
activeNode: ActiveNode,
ancestorIndex: number,
n: number = 1,
maybeAllowedNodeKinds: ReadonlyArray<Ast.NodeKind> | undefined = undefined,
): TXorNode {
const maybeXorNode: TXorNode | undefined = maybePreviousXorNode(activeNode, ancestorIndex, n);
if (maybeXorNode === undefined) {
throw new CommonError.InvariantError("no previous node");
}
const xorNode: TXorNode = maybeXorNode;

if (maybeAllowedNodeKinds !== undefined) {
const maybeErr: CommonError.InvariantError | undefined = NodeIdMapUtils.testAstAnyNodeKind(
xorNode,
maybeAllowedNodeKinds,
);
if (maybeErr) {
throw maybeErr;
}
}

return maybeXorNode;
}

export function expectNextXorNode(
activeNode: ActiveNode,
ancestorIndex: number,
n: number = 1,
maybeAllowedNodeKinds: ReadonlyArray<Ast.NodeKind> | undefined = undefined,
): TXorNode {
const maybeXorNode: TXorNode | undefined = maybeNextXorNode(activeNode, ancestorIndex, n);
if (maybeXorNode === undefined) {
throw new CommonError.InvariantError("no next node");
}
const xorNode: TXorNode = maybeXorNode;

if (maybeAllowedNodeKinds !== undefined) {
const maybeErr: CommonError.InvariantError | undefined = NodeIdMapUtils.testAstAnyNodeKind(
xorNode,
maybeAllowedNodeKinds,
);
if (maybeErr) {
throw maybeErr;
}
}

return maybeXorNode;
}

interface AstNodeSearch {
readonly maybeNode: Ast.TNode | undefined;
readonly maybeShiftedNode: Ast.TNode | undefined;
Expand Down
51 changes: 29 additions & 22 deletions src/inspection/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,32 @@
import { CommonError, Result } from "../common";
import { ResultUtils } from "../common/result";
import { KeywordKind, TExpressionKeywords, Token, TokenKind } from "../lexer";
import { Ast, IParserState, NodeIdMap, NodeIdMapUtils, ParseError, TXorNode, XorNodeKind } from "../parser";
import { InspectionSettings } from "../settings";
import { ActiveNode, ActiveNodeUtils } from "./activeNode";
import {
AncestryUtils,
Ast,
IParserState,
NodeIdMap,
NodeIdMapUtils,
ParseError,
TXorNode,
XorNodeKind,
} from "../parser";
import { CommonSettings } from "../settings";
import { ActiveNode } from "./activeNode";
import { Position, PositionUtils } from "./position";

export interface InspectedAutocomplete {
readonly autocompleteKeywords: ReadonlyArray<KeywordKind>;
}
export type Autocomplete = ReadonlyArray<KeywordKind>;

export type TriedAutocomplete = Result<InspectedAutocomplete, CommonError.CommonError>;
export type TriedAutocomplete = Result<Autocomplete, CommonError.CommonError>;

export function tryInspectAutocomplete<S = IParserState>(
settings: InspectionSettings,
export function tryAutocomplete<S = IParserState>(
settings: CommonSettings,
maybeActiveNode: ActiveNode | undefined,
nodeIdMapCollection: NodeIdMap.Collection,
maybeParseError: ParseError.ParseError<S> | undefined,
): TriedAutocomplete {
if (maybeActiveNode === undefined) {
return ResultUtils.okFactory({
autocompleteKeywords: ExpressionAutocomplete,
});
return ResultUtils.okFactory(ExpressionAutocomplete);
}
const activeNode: ActiveNode = maybeActiveNode;

Expand Down Expand Up @@ -65,15 +70,15 @@ export function tryInspectAutocomplete<S = IParserState>(
);
inspected = filterRecommendations(inspected, maybePositionName);

return ResultUtils.okFactory({ autocompleteKeywords: inspected });
return ResultUtils.okFactory(inspected);
}

// Travel the ancestry path in Active node in [parent, child] pairs.
// Without zipping the values we wouldn't know what we're completing for.
// For example 'if true |' gives us a pair something like [IfExpression, Constant].
// We can now know we failed to parse a 'then' constant.
function traverseAncestors(
settings: InspectionSettings,
settings: CommonSettings,
activeNode: ActiveNode,
nodeIdMapCollection: NodeIdMap.Collection,
maybeParseErrorToken: Token | undefined,
Expand Down Expand Up @@ -338,7 +343,7 @@ function autocompleteErrorHandlingExpression(
function autocompleteListExpression(
activeNode: ActiveNode,
child: TXorNode,
ancestorIndex: number,
ancestryIndex: number,
): ReadonlyArray<KeywordKind> | undefined {
// '{' or '}'
if (child.node.maybeAttributeIndex === 0 || child.node.maybeAttributeIndex === 2) {
Expand All @@ -352,7 +357,7 @@ function autocompleteListExpression(
}

// ListExpression -> ArrayWrapper -> Csv -> X
const nodeOrComma: TXorNode = ActiveNodeUtils.expectPreviousXorNode(activeNode, ancestorIndex, 3, undefined);
const nodeOrComma: TXorNode = AncestryUtils.expectPreviousXorNode(activeNode.ancestry, ancestryIndex, 3, undefined);
if (nodeOrComma.node.maybeAttributeIndex !== 0) {
return undefined;
}
Expand All @@ -361,7 +366,7 @@ function autocompleteListExpression(
// but we have to drill down one more level if it's a RangeExpression.
const itemNode: TXorNode =
nodeOrComma.node.kind === Ast.NodeKind.RangeExpression
? ActiveNodeUtils.expectPreviousXorNode(activeNode, ancestorIndex, 4, undefined)
? AncestryUtils.expectPreviousXorNode(activeNode.ancestry, ancestryIndex, 4, undefined)
: nodeOrComma;

if (itemNode.kind === XorNodeKind.Context || PositionUtils.isBeforeXorNode(activeNode.position, itemNode, false)) {
Expand All @@ -378,7 +383,7 @@ function autocompleteSectionMember(
activeNode: ActiveNode,
parent: TXorNode,
child: TXorNode,
ancestorIndex: number,
ancestryIndex: number,
): ReadonlyArray<KeywordKind> | undefined {
// SectionMember.namePairedExpression
if (child.node.maybeAttributeIndex === 2) {
Expand All @@ -395,10 +400,12 @@ function autocompleteSectionMember(
}

// SectionMember -> IdentifierPairedExpression -> Identifier
const maybeName: TXorNode | undefined = ActiveNodeUtils.maybePreviousXorNode(activeNode, ancestorIndex, 2, [
Ast.NodeKind.IdentifierPairedExpression,
Ast.NodeKind.Identifier,
]);
const maybeName: TXorNode | undefined = AncestryUtils.maybePreviousXorNode(
activeNode.ancestry,
ancestryIndex,
2,
[Ast.NodeKind.IdentifierPairedExpression, Ast.NodeKind.Identifier],
);

// Name hasn't been parsed yet so we can exit.
if (maybeName === undefined || maybeName.kind !== XorNodeKind.Ast) {
Expand Down
5 changes: 1 addition & 4 deletions src/inspection/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import * as InspectionUtils from "./inspectionUtils";

export { InspectionUtils };
export * from "./autocomplete";
export * from "./inspection";
export * from "./position";
export * from "./scope";
export * from "./type";
export * from "./invokeExpression";
74 changes: 0 additions & 74 deletions src/inspection/inspection.ts

This file was deleted.

Loading

0 comments on commit 2745e67

Please sign in to comment.