Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): improve scope-analysis types #1481

Merged
merged 5 commits into from Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion packages/experimental-utils/src/ts-eslint-scope/Reference.ts
Expand Up @@ -3,21 +3,36 @@ import ESLintReference from 'eslint-scope/lib/reference';
import { Scope } from './Scope';
import { Variable } from './Variable';

export type ReferenceFlag = 0x1 | 0x2 | 0x3;

interface Reference {
identifier: TSESTree.Identifier;
from: Scope;
resolved: Variable | null;
writeExpr: TSESTree.Node | null;
init: boolean;

partial: boolean;
__maybeImplicitGlobal: boolean;
tainted?: boolean;
typeMode?: boolean;

isWrite(): boolean;
isRead(): boolean;
isWriteOnly(): boolean;
isReadOnly(): boolean;
isReadWrite(): boolean;
}
const Reference = ESLintReference as {
new (): Reference;
new (
identifier: TSESTree.Identifier,
scope: Scope,
flag?: ReferenceFlag,
writeExpr?: TSESTree.Node | null,
maybeImplicitGlobal?: boolean,
partial?: boolean,
init?: boolean,
): Reference;

READ: 0x1;
WRITE: 0x2;
Expand Down
4 changes: 2 additions & 2 deletions packages/experimental-utils/src/ts-eslint-scope/Referencer.ts
Expand Up @@ -44,8 +44,8 @@ interface Referencer<SM extends ScopeManager> extends Visitor {

AssignmentExpression(node: TSESTree.Node): void;
CatchClause(node: TSESTree.Node): void;
Program(node: TSESTree.Node): void;
Identifier(node: TSESTree.Node): void;
Program(node: TSESTree.Program): void;
Identifier(node: TSESTree.Identifier): void;
UpdateExpression(node: TSESTree.Node): void;
MemberExpression(node: TSESTree.Node): void;
Property(node: TSESTree.Node): void;
Expand Down
26 changes: 14 additions & 12 deletions packages/experimental-utils/src/ts-eslint-scope/Scope.ts
Expand Up @@ -15,7 +15,7 @@ import {
ClassScope as ESLintClassScope,
} from 'eslint-scope/lib/scope';
import { Definition } from './Definition';
import { Reference } from './Reference';
import { Reference, ReferenceFlag } from './Reference';
import { ScopeManager } from './ScopeManager';
import { Variable } from './Variable';

Expand Down Expand Up @@ -46,35 +46,37 @@ interface Scope {
references: Reference[];
through: Reference[];
thisFound?: boolean;
taints: Map<string, boolean>;
functionExpressionScope: boolean;
__left: Reference[];

__shouldStaticallyClose(scopeManager: ScopeManager): boolean;
__shouldStaticallyCloseForGlobal(ref: any): boolean;
__staticCloseRef(ref: any): void;
__dynamicCloseRef(ref: any): void;
__globalCloseRef(ref: any): void;
__close(scopeManager: ScopeManager): Scope;
__isValidResolution(ref: any, variable: any): boolean;
__resolve(ref: any): boolean;
__isValidResolution(ref: any, variable: any): variable is Variable;
__resolve(ref: Reference): boolean;
__delegateToUpperScope(ref: any): void;
__addDeclaredVariablesOfNode(variable: any, node: TSESTree.Node): void;
__defineGeneric(
name: any,
set: any,
variables: any,
node: any,
name: string,
set: Map<string, Variable>,
variables: Variable[],
node: TSESTree.Identifier,
def: Definition,
): void;

__define(node: TSESTree.Node, def: Definition): void;

__referencing(
node: TSESTree.Node,
assign: number,
writeExpr: TSESTree.Node,
maybeImplicitGlobal: any,
partial: any,
init: any,
assign?: ReferenceFlag,
writeExpr?: TSESTree.Node,
maybeImplicitGlobal?: any,
partial?: any,
init?: any,
): void;

__detectEval(): void;
Expand Down
Expand Up @@ -16,6 +16,9 @@ interface ScopeManagerOptions {
interface ScopeManager {
__options: ScopeManagerOptions;
__currentScope: Scope;
__nodeToScope: WeakMap<TSESTree.Node, Scope[]>;
__declaredVariables: WeakMap<TSESTree.Node, Variable[]>;

scopes: Scope[];
globalScope: Scope;

Expand All @@ -28,15 +31,15 @@ interface ScopeManager {
isStrictModeSupported(): boolean;

// Returns appropriate scope for this node.
__get(node: TSESTree.Node): Scope;
__get(node: TSESTree.Node): Scope | undefined;
getDeclaredVariables(node: TSESTree.Node): Variable[];
acquire(node: TSESTree.Node, inner?: boolean): Scope | null;
acquireAll(node: TSESTree.Node): Scope | null;
release(node: TSESTree.Node, inner?: boolean): Scope | null;
attach(): void;
detach(): void;

__nestScope(scope: Scope): Scope;
__nestScope<T extends Scope>(scope: T): T;
__nestGlobalScope(node: TSESTree.Node): Scope;
__nestBlockScope(node: TSESTree.Node): Scope;
__nestFunctionScope(node: TSESTree.Node, isMethodDefinition: boolean): Scope;
Expand Down
4 changes: 4 additions & 0 deletions packages/experimental-utils/src/ts-eslint-scope/Variable.ts
Expand Up @@ -2,13 +2,17 @@ import { TSESTree } from '@typescript-eslint/typescript-estree';
import ESLintVariable from 'eslint-scope/lib/variable';
import { Reference } from './Reference';
import { Definition } from './Definition';
import { Scope } from './Scope';

interface Variable {
name: string;
identifiers: TSESTree.Identifier[];
references: Reference[];
defs: Definition[];
eslintUsed?: boolean;
stack?: unknown;
tainted?: boolean;
scope?: Scope;
}

const Variable = ESLintVariable as {
Expand Down
99 changes: 54 additions & 45 deletions packages/parser/tests/tools/scope-analysis.ts
@@ -1,28 +1,33 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
TSESTree,
TSESLintScope,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';
import { ScopeManager } from '../../src/scope/scope-manager';

/** Reference resolver. */
export class ReferenceResolver {
map: Map<any, any>;
export class ReferenceResolver<TKey = unknown> {
map: Map<TKey, { $id: number }>;

constructor() {
this.map = new Map();
}

resolve(obj: any, properties: any): any {
const resolved = Object.assign({ $id: this.map.size }, properties);
resolve<T>(obj: TKey, properties: T): T & { $id: number } {
const resolved = { ...properties, $id: this.map.size };
armano2 marked this conversation as resolved.
Show resolved Hide resolved
this.map.set(obj, resolved);
return resolved;
}

ref(obj: any): any {
ref(obj: TKey): { $ref: number } | TKey {
if (typeof obj !== 'object' || obj === null) {
return obj;
}

const map = this.map;
return {
get $ref(): any {
return map.get(obj).$id;
get $ref(): number {
return map.get(obj)!.$id;
},
};
}
Expand All @@ -31,30 +36,38 @@ export class ReferenceResolver {
/**
* Convert a given node object to JSON object.
* This saves only type and range to know what the node is.
* @param {ASTNode} node The AST node object.
* @returns {Object} The object that can be used for JSON.stringify.
* @param node The AST node object.
* @returns The object that can be used for JSON.stringify.
*/
export function nodeToJSON(node: any): any {
export function nodeToJSON(
node: TSESTree.Node | null | undefined,
):
| { type: AST_NODE_TYPES; range: [number, number]; name?: string }
| null
| undefined {
if (!node) {
return node;
}

const { type, name, range } = node;
if (node.type === 'Identifier') {
return { type, name, range };
const { type, range } = node;
if (node.type === AST_NODE_TYPES.Identifier) {
return { type, name: node.name, range };
}
return { type, range };
}

/**
* Convert a given variable object to JSON object.
* @param {Variable} variable The eslint-scope's variable object.
* @param {ReferenceResolver} resolver The reference resolver.
* @returns {Object} The object that can be used for JSON.stringify.
* @param variable The eslint-scope's variable object.
* @param resolver The reference resolver.
* @returns The object that can be used for JSON.stringify.
*/
export function variableToJSON(variable: any, resolver: any): any {
export function variableToJSON(
variable: TSESLintScope.Variable,
resolver: ReferenceResolver,
): unknown {
const { name, eslintUsed } = variable;
const defs = variable.defs.map((d: any) => ({
const defs = variable.defs.map(d => ({
type: d.type,
name: nodeToJSON(d.name),
node: nodeToJSON(d.node),
Expand All @@ -76,11 +89,14 @@ export function variableToJSON(variable: any, resolver: any): any {

/**
* Convert a given reference object to JSON object.
* @param {Reference} reference The eslint-scope's reference object.
* @param {ReferenceResolver} resolver The reference resolver.
* @returns {Object} The object that can be used for JSON.stringify.
* @param reference The eslint-scope's reference object.
* @param resolver The reference resolver.
* @returns The object that can be used for JSON.stringify.
*/
export function referenceToJSON(reference: any, resolver: any): any {
export function referenceToJSON(
reference: TSESLintScope.Reference,
resolver: ReferenceResolver,
): unknown {
const kind = `${reference.isRead() ? 'r' : ''}${
reference.isWrite() ? 'w' : ''
}`;
Expand All @@ -100,35 +116,28 @@ export function referenceToJSON(reference: any, resolver: any): any {

/**
* Convert a given scope object to JSON object.
* @param {Scope} scope The eslint-scope's scope object.
* @param {ReferenceResolver} resolver The reference resolver.
* @param scope The eslint-scope's scope object.
* @param resolver The reference resolver.
* @returns {Object} The object that can be used for JSON.stringify.
*/
export function scopeToJSON(
scope: any,
scope: TSESLintScope.Scope,
resolver = new ReferenceResolver(),
): any {
): unknown {
const { type, functionExpressionScope, isStrict } = scope;
const block = nodeToJSON(scope.block);
const variables = scope.variables.map((v: any) =>
variableToJSON(v, resolver),
);
const references = scope.references.map((r: any) =>
referenceToJSON(r, resolver),
);
const variableMap = Array.from(scope.set.entries()).reduce(
(map: any, [name, variable]: any) => {
map[name] = resolver.ref(variable);
return map;
},
{},
);
const variables = scope.variables.map(v => variableToJSON(v, resolver));
const references = scope.references.map(r => referenceToJSON(r, resolver));
const variableMap = Array.from(scope.set.entries()).reduce<
Record<string, unknown>
>((map, [name, variable]) => {
map[name] = resolver.ref(variable);
return map;
}, {});
const throughReferences = scope.through.map(resolver.ref, resolver);
const variableScope = resolver.ref(scope.variableScope);
const upperScope = resolver.ref(scope.upper);
const childScopes = scope.childScopes.map((c: any) =>
scopeToJSON(c, resolver),
);
const childScopes = scope.childScopes.map(c => scopeToJSON(c, resolver));

return resolver.resolve(scope, {
type,
Expand All @@ -145,12 +154,12 @@ export function scopeToJSON(
});
}

export function getScopeTree(scopeManager: any): any {
export function getScopeTree(scopeManager: ScopeManager): unknown {
const { globalScope } = scopeManager;

// Do the postprocess to test.
// https://github.com/eslint/eslint/blob/84ce72fdeba082b7b132e4ac6b714fb1a93831b7/lib/linter.js#L112-L129
globalScope.through = globalScope.through.filter((reference: any) => {
globalScope.through = globalScope.through.filter(reference => {
const name = reference.identifier.name;
const variable = globalScope.set.get(name);
if (variable) {
Expand Down