apigeelint
Version:
Node module and tool to lint a bundle for an Apigee API Proxy or sharedflow.
147 lines (125 loc) • 3.65 kB
JavaScript
// Copyright © 2026 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
const headerVarRe = new RegExp(
"^([a-zA-z0-9_][-a-zA-z0-9_]*)\\.header\\.([a-zA-z0-9_][-a-zA-z0-9_]*)$",
);
/*
* Transform to Negation Normal Format. This logic
* normalizes "NOT (A OR B) into "NOT(A) and NOT(B)".
**/
function toNNF(node) {
// Base case: Literals/Strings
if (typeof node === "string") return node;
const { operator, operands } = node;
// Handle Negations
if (operator === "NOT") {
const child = operands[0];
// Double Negation: NOT(NOT(A)) => A
if (child.operator === "NOT") {
return toNNF(child.operands[0]);
}
// De Morgan: NOT(A AND B) => (NOT A) OR (NOT B)
if (child.operator === "AND") {
return toNNF({
operator: "OR",
operands: child.operands.map((op) => ({
operator: "NOT",
operands: [op],
})),
});
}
// De Morgan: NOT(A OR B) => (NOT A) AND (NOT B)
if (child.operator === "OR") {
return toNNF({
operator: "AND",
operands: child.operands.map((op) => ({
operator: "NOT",
operands: [op],
})),
});
}
// If it's a leaf we can't simplify further
return { operator, operands: [toNNF(child)] };
}
// String comparisons may take variables referring to headers.
// In that case, normalize header names to uppercase.
if (
[
"EqualsCaseInsensitive",
"Equals",
"NotEquals",
"StartsWith",
"Matches",
"MatchesPath",
"JavaRegex",
].includes(operator)
) {
const m = headerVarRe.exec(operands[0]); // left operand
if (m) {
operands[0] = `${m[1]}.header.${m[2].toUpperCase()}`;
}
}
// normalize NotEquals to Not(Equals): (A != B) => NOT(A = B)
if (operator === "NotEquals") {
return {
operator: "NOT",
operands: [
{
operator: "Equals",
operands,
},
],
};
}
// Standard AND/OR (Just recurse)
return {
operator,
operands: operands.map(toNNF),
};
}
function canonicalize(node) {
// Convert to NNF first to handle De Morgan's and NOTs
const nnfNode = toNNF(node);
function process(n) {
if (typeof n === "string") {
return { node: n, signature: n };
}
let ops = n.operands.map(process);
// Flattening (Associativity)
if (n.operator === "AND" || n.operator === "OR") {
ops = ops.flatMap((child) =>
child.node.operator === n.operator
? child.node.operands.map(process)
: [child],
);
// commutativity
ops.sort((a, b) => a.signature.localeCompare(b.signature));
}
const finalOperands = ops.map((o) => o.node);
const finalSig = `${n.operator}(${ops.map((o) => o.signature).join(",")})`;
return {
node: { operator: n.operator, operands: finalOperands },
signature: finalSig,
};
}
return process(nnfNode);
}
function equivalent(expr1, expr2) {
return canonicalize(expr1).signature === canonicalize(expr2).signature;
}
module.exports = {
equivalent,
canonicalize,
};