ast-deep-contains1.1.21

Like t.same assert on array of objects, where element order doesn't matter.

§ Quick Take

import { strict as assert } from "assert";
import deepContains from "ast-deep-contains";

const gathered = [];
const errors = [];

const reference = [
  { c: "2" }, // will end up not used
  { a: "1", b: "2", c: "3" },
  { x: "8", y: "9", z: "0" },
];

const structureToMatch = [
  { a: "1", b: "2", c: "3" }, // matches but has different position in the source
  { x: "8", y: "9" }, // "z" missing
];

// This program pre-matches first, then matches objects as a set-subset
deepContains(
  reference,
  structureToMatch,
  (leftSideVal, rightSideVal) => {
    // This callback does the pre-matching and picks the key pairs for you.
    // It's up to you what you will do with left- and right-side
    // values - we normally feed them to unit test asserts but here we just push
    // to array:
    gathered.push([leftSideVal, rightSideVal]);
  },
  (err) => {
    errors.push(err);
  }
);

// imagine instead of pushing pairs into array, you fed them into assert
// function in unit tests:
assert.deepEqual(gathered, [
  ["1", "1"],
  ["2", "2"],
  ["3", "3"],
  ["8", "8"],
  ["9", "9"],
]);
assert.equal(errors.length, 0);

§ Purpose

This is a fancy assertion to match arrays of objects, where order doesn't matter and the reference objects might have extra keys. This program really tries to find matches.

We had a situation in emlint — error objects come in asynchronous fashion so their order is pretty random. Yet, we want to assert, does the error array contain error object X.

Another consideration is that result error object might have extra keys we don't care to match — for example, row and column numbers.

[
{
ruleId: "tag-is-present",
line: 1, // not present in matched object
column: 4, // not present in matched object
severity: 2, // not present in matched object
idxFrom: 0,
idxTo: 4,
message: "h1 is not allowed.",
fix: {
ranges: [[0, 4]],
},
}
]

vs

[
{
ruleId: "tag-is-present",
idxFrom: 43,
idxTo: 48,
message: "h1 is not allowed.",
fix: {
ranges: [[43, 48]],
},
},
];

Notice how above we don't bother with line and column values, as well as severity. Also, note that key structure is very similar, yet objects are in a wrong order (because rules were raised in such way).

Ava's t.deepEqual is exact match so 1) missing keys and 2) wrong object order in the array would be an issue.

Tap's t.same would match set/subset keys but would still not be able to detect that two objects are in a wrong order.

Solution is this package.

It will try to match which object is the most similar to the source's, then will not raise errors if source has extra keys.

Matching is passed to your chosen assertion functions, most likely t.is and t.fail.

§ Example #1 — checking subset of keys only

Here is reduced example based on codsen-tokenizer tests:

const t = require("tap");
import ct from "codsen-tokenizer";
import deepContains from "ast-deep-contains";

test("01.01 — text-tag-text", (t) => {
const gathered = [];
ct(" <a>z", (obj) => {
gathered.push(obj);
});

deepContains(
gathered,
[
{
type: "text",
start: 0,
end: 2,
// <- tokenizer reports way more keys than that
},
{
type: "html",
start: 2,
end: 5,
},
{
type: "text",
start: 5,
end: 6,
},
],
t.is, // each pair of keys is ran by this function
t.fail // major failures are pinged to this function
);
});

In example above, reported objects will have more keys than what's compared. Throughout the time, when improving the tokenizer we will surely add even more new keys. All this should not affect the main keys. Using t.same would be a nuisance — we'd have to update all unit tests each time after a new improvement to the tokenizer is done, new key is added.

§ Example #2 — matching array contents, order is random

Our linter emlint is pluggable — each rule is a plugin and program's architecture is based on the Observer patten — the main checking function in EMLint is extending the Node's EventEmitter class:

class Linter extends EventEmitter {
...
}

This means, the nature in which errors are raised is somewhat undetermined. In EMLint unit tests we want to check, were correct errors raised and would the proposed string fixing index ranges fix the input.

Same way with the yard's dog and cat example, we don't care about the order of the pets (linter error objects) — as long each one of the set is reported, happy days.

Behold - a program flags up two backslashes on a void HTML tag — the first backslash should be deleted, second one turned into normal slash — we don't care about the order of the elements as long as all elements were matched, plus there might be extra keys in the source objects — source objects are superset of what we're matching:

const t = require("tap");
import { Linter } from "emlint";
import deepContains from "ast-deep-contains"; // <------------ this program
import { applyFixes } from "t-util/util";

const BACKSLASH = "\u005C";

t.test(
`06.01 - ${`\u001b[${36}m${`both sides`}\u001b[${39}m`} - extreme case`,
(t) => {
const str = `<${BACKSLASH}br${BACKSLASH}>`;
const linter = new Linter();
// call the linter and record the result's error messages:
const messages = linter.verify(str, {
rules: {
tag: 2,
},
});
// assertion:
deepContains(
messages,
[
{
ruleId: "tag-closing-backslash",
severity: 2,
idxFrom: 1,
idxTo: 2,
message: "Wrong slash - backslash.",
fix: {
ranges: [[1, 2]],
},
// <---- "messages" we're comparing against will have more keys but we don't care
},
{
ruleId: "tag-closing-backslash",
severity: 2,
idxFrom: 4,
idxTo: 5,
message: "Replace backslash with slash.",
fix: {
ranges: [[4, 5, "/"]],
},
},
],
t.is, // each pair of key values is ran by this function
t.fail // major failures are pinged to this function
);
}
);

The order in which backslashes will be reported does not matter, plus Linter might report more information — that's welcomed but will be ignored, not a cause for error.

§ API

deepContains(tree1, tree2, cb, errCb[, opts])

in other words, it's a function which takes 5 input arguments:

Input argumentTypeObligatory?Description
tree1reference AST, can be superset of tree2yesArray, object or nested thereof
tree2AST being checked, can be subset of tree1yesArray, object or nested thereof
cbfunctionyesThis function will be called with pairs, of values from each path. Think t.is of AVA. See API below.
errCbfunctionyesIf path does not exist on tree1, this callback function will be called with a message string. Think t.fail of AVA.
optsPlain objectnoOptional plain object containing settings, see API below.

Program returns undefined because it's operated by callbacks.

§ Options object

options object's keyTypeDefaultDescription
skipContainersBooleantrueDuring traversal, containers (arrays and objects) will be checked for existence and traversed further but callback won't be pinged. Set to false to stop doing that.
arrayStrictComparisonBooleanfalseObjects in the array can be of random order, as long as each one is matched, order does not matter. For strict order, set to true.

Here is the defaults object, in one place, if you need to copy it:

{
skipContainers: true,
arrayStrictComparison: false
}

§ opts.skipContainers

Consider these two AST's, for example:

Object 1:

{
"a": {
"b": "c"
}
}

Object 2:

{
"a": {
"b": "d"
}
}

During traversal, monkey will check for existence of path "a" on Object 1 but won't report the values {"b": "c"} and {"b": "d"}. This way, when using this program in unit test context, AVA's t.is can be passed and we would be matching the values only. Missing paths would get reported to AVA's t.fail.

Let me repeat, no matter the setting on opts.skipContainers, in example above, the existence of the a will be checked, it's just that the values, objects, won't be passed to a callback, because they might be not equal either — first one might be superset!

§ API's Output

Output is undefined — this program is used exclusively through callbacks. Those do the job — function does not return anything.

§ Licence

MIT opens in a new tab

Copyright © 2010–2020 Roy Revelt and other contributors

Related packages:

📦 emlint 2.18.17
Pluggable email template code linter
📦 codsen-tokenizer 2.17.7
HTML and CSS lexer aimed at code with fatal errors, accepts mixed coding languages
📦 codsen-parser 0.7.7
Parser aiming at broken or mixed code, especially HTML & CSS
📦 ast-monkey-traverse-with-lookahead 1.1.12
Utility library to traverse AST, reports upcoming values
📦 ast-is-empty 1.10.10
Find out, is nested array/object/string/AST tree is empty
📦 ast-contains-only-empty-space 1.9.16
Returns Boolean depending if passed AST contain only empty space
📦 ast-delete-object 1.9.2
Delete all plain objects in AST if they contain a certain key/value pair