FarmBot/Farmbot-Web-App

View on GitHub
frontend/sequences/locals_list/sanitize_nodes.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { cloneDeep, get, uniq } from "lodash";
import {
  Dictionary,
  Identifier,
  ScopeDeclarationBodyItem,
  VariableDeclaration,
  Execute,
} from "farmbot";
import {
  SequenceResource as Sequence,
} from "farmbot/dist/resources/api_resources";
import { maybeTagStep } from "../../resources/sequence_tagging";
import { newVariableDataValue, varTypeFromLabel } from "./new_variable";

// ======= TYPE DECLARATIONS =======
/** Less strict version of CeleryScript args. It's traversable, or unknown. */
type Args = Dictionary<unknown>;
type Body = Traversable[] | undefined;
/** Less strict CeleryScript node used for the sake of recursion. */
export interface Traversable { kind: string; args: Args; body?: Body; }
type TreeClimberCB = (item: Traversable) => void;
type InterestingNodes = Identifier | Execute;
// ======= END TYPE DECLARATIONS =======

// ======= CONST / LITERAL / DYNAMIC KEY DECLARATIONS =======
const ARGS: keyof InterestingNodes = "args";
const KIND: keyof InterestingNodes = "kind";
const IDENTIFIER: Identifier["kind"] = "identifier";
const EXECUTE: Execute["kind"] = "execute";
const OBJECT = "object";
const STRING = "string";
// ======= END CONST / LITERAL DECLARATIONS =======

/** Is it a fully-formed CeleryScript node? Can we continue recursing? */
const isTraversable = (x: unknown): x is Traversable => {
  const hasKind = typeof get(x, KIND, -1) == STRING;
  const hasArgs = typeof get(x, ARGS, -1) == OBJECT;
  return hasKind && hasArgs;
};

/** Is it a variable (identifier)? */
const isIdentifier =
  (x: Traversable): x is Identifier => (x.kind === IDENTIFIER);

/** Is it an execute block? */
const isExecute = (x: Traversable): x is Execute => {
  return !!((x.kind === EXECUTE) && (x as Execute).args.sequence_id);
};

const newVar = (label: string): VariableDeclaration => ({
  kind: "variable_declaration",
  args: { label, data_value: newVariableDataValue(varTypeFromLabel(label)) }
});

function climb(t: unknown, cb: TreeClimberCB) {
  const climbArgs = /** RECURSION ALERT! */
    (a: Args) => Object.keys(a).map(arg => climb(a[arg], cb));
  const climbBody = /** WEE OOO WEE OO */
    (body: Traversable[]) => body.map(item => climb(item, cb));

  if (isTraversable(t)) {
    t.body = t.body || [];
    climbArgs(t.args);
    climbBody(t.body);
    cb(t);
  }
}

interface SanitizationResult {
  thisSequence: Sequence;
  callsTheseSequences: number[];
}

/* 1. Recursively tag all CeleryScript nodes with a `uuid` property to
 *    prevent subtle React issues. SEE: Explanation in `sequence_tagging.ts`
 * 2. Add unbound variables to `locals` declaration (prevent NPEs).
 */
export const sanitizeNodes = (thisSequence: Sequence): SanitizationResult => {
  // Collect all *declared* variables.
  const declared: Dictionary<ScopeDeclarationBodyItem> = {};
  (thisSequence.args.locals.body || []).map(variable =>
    declared[variable.args.label] = variable);
  // Collect all *referenced* variables.
  const { id } = thisSequence;
  const used: Dictionary<Identifier> = {};
  const collectUniqVariables = (identifier: Identifier) =>
    used[identifier.args.label] = identifier;
  const idList: number[] = [];
  climb(thisSequence, node => {
    maybeTagStep(node);
    isIdentifier(node) && collectUniqVariables(node);
    // Collect "in_use" sequences.
    if (isExecute(node)) {
      const { sequence_id } = node.args;
      // Recursion does not qualify as "in_use"
      (sequence_id != id) && idList.push(sequence_id);
    }
  });
  // Collect all unused variables.
  const unused: ScopeDeclarationBodyItem[] = Object.values(declared)
    .filter(var_ => !Object.keys(used).includes(var_.args.label));
  // Add unbound variables to locals array.
  thisSequence.args.locals.body = Object.values(used)
    .map(({ args }) => declared[args.label] || newVar(args.label))
    .concat(unused)
    .map(node => {
      maybeTagStep(node);
      return node;
    });

  return { thisSequence, callsTheseSequences: uniq(idList) };
};

export const variableIsInUse = (sequence: Sequence | undefined, label: string) => {
  const usedLabels: string[] = [];
  climb(cloneDeep(sequence),
    node => isIdentifier(node) && usedLabels.push(node.args.label));
  return usedLabels.includes(label);
};