🔥 Publicodes static cycles check using AST iterator

* AST API: add AST visitor & iterator
* cycles detection: use AST iterator
* remove dagrejs/graphlib
* cycle extraction: smallest cycle & print in Graphviz dot format

WARNING: a cycle still exists around `entreprise . chiffre d'affaires` see
issue #1524 for a definitive fix.
pull/1564/head
Alexandre Hajjar 2021-05-04 19:40:10 +02:00
parent 884c4239b0
commit c5d80fae71
14 changed files with 604 additions and 158 deletions

View File

@ -4,31 +4,34 @@ import { cyclicDependencies } from '../../publicodes/core/source/AST/graph'
describe('DottedNames graph', () => {
it("shouldn't have cycles", () => {
let cyclesDependencies = cyclicDependencies(rules)
const [cyclesDependencies, dotGraphs] = cyclicDependencies(rules)
const dotGraphsToLog = dotGraphs
.map(
(dotGraph) =>
`🌀🌀🌀🌀🌀🌀🌀🌀🌀🌀🌀\n A cycle graph to stare at with Graphviz:\n${dotGraph}\n\n`
)
.join('\n\n')
expect(
cyclesDependencies,
`\nThe cycles have been found in the rules dependencies graph.\nSee below for a representation of each cycle.\n⬇️ is a node of the cycle.\n\t- ${cyclesDependencies
`${dotGraphsToLog}\nAT LEAST the following cycles have been found in the rules dependencies graph.\nSee below for a representation of each cycle.\n⬇️ is a node of the cycle.\n\t- ${cyclesDependencies
.map(
(cycleDependencies, idx) =>
'#' +
idx +
':\n\t\t⬇ ' +
cycleDependencies
// .map(
// ([ruleName, dependencies]) =>
// ruleName + '\n\t\t\t↘ ' + dependencies.join('\n\t\t\t↘ ')
// )
.join('\n\t\t⬇ ')
'#' + idx + ':\n\t\t⬇ ' + cycleDependencies.join('\n\t\t⬇ ')
)
.join('\n\t- ')}\n\n`
).to.deep.equal([
[
"entreprise . chiffre d'affaires",
'dirigeant . rémunération . impôt',
'dirigeant . rémunération . imposable',
'dirigeant . auto-entrepreneur . impôt . revenu imposable',
"entreprise . chiffre d'affaires . vente restauration hébergement",
],
])
console.warn(
"[ WARNING ] A cycle still exists around `entreprise . chiffre d'affaires` see issue #1524 for a definitive fix."
)
.to.be.an('array')
.of.length(1)
// Cycle doesn't occur in real life.
// ⬇️ entreprise . chiffre d'affaires
// ⬇️ dirigeant . rémunération totale
// ⬇️ entreprise . chiffre d'affaires
})
})

View File

@ -28,8 +28,7 @@
"chai": "^4.2.0",
"intl": "^1.2.5",
"typescript": "^4.2.4",
"dedent-js": "1.0.1",
"@dagrejs/graphlib": "^2.1.4"
"dedent-js": "1.0.1"
},
"dependencies": {
"moo": "^0.5.1",

View File

@ -0,0 +1,330 @@
/* eslint-disable prefer-rest-params */
/* eslint-disable @typescript-eslint/no-this-alias */
// Adapted from https://github.com/dagrejs/graphlib (MIT license)
// and https://github.com/lodash/lodash (MIT license)
// TODO: type this
function has(obj, key) {
return obj != null && Object.prototype.hasOwnProperty.call(obj, key)
}
function constant(value) {
return function (...args) {
return value
}
}
const DEFAULT_EDGE_NAME = '\x00'
const EDGE_KEY_DELIM = '\x01'
const incrementOrInitEntry = (map, k) => {
if (map[k]) {
map[k]++
} else {
map[k] = 1
}
}
const decrementOrRemoveEntry = (map, k) => {
if (!--map[k]) {
delete map[k]
}
}
const edgeArgsToId = (isDirected, v_, w_, name) => {
let v = '' + v_
let w = '' + w_
if (!isDirected && v > w) {
const tmp = v
v = w
w = tmp
}
return (
v +
EDGE_KEY_DELIM +
w +
EDGE_KEY_DELIM +
(name === undefined ? DEFAULT_EDGE_NAME : name)
)
}
const edgeArgsToObj = (isDirected, v_, w_, name) => {
let v = '' + v_
let w = '' + w_
if (!isDirected && v > w) {
const tmp = v
v = w
w = tmp
}
const edgeObj: any = { v: v, w: w }
if (name) {
edgeObj.name = name
}
return edgeObj
}
const edgeObjToId = (isDirected, edgeObj) => {
return edgeArgsToId(isDirected, edgeObj.v, edgeObj.w, edgeObj.name)
}
export class Graph {
private _nodeCount = 0
private _edgeCount = 0
private _isDirected: any
private _label: undefined
private _defaultNodeLabelFn: (...args: any[]) => any
private _defaultEdgeLabelFn: (...args: any[]) => any
private _nodes: Record<string, any>
private _in: Record<string, any>
private _preds: Record<string, Record<string, number>>
private _out: Record<string, Record<string, string>>
private _sucs: Record<string, Record<string, number>>
private _edgeObjs: Record<any, any>
private _edgeLabels: Record<any, string>
constructor(opts: Record<string, boolean> = {}) {
this._isDirected = has(opts, 'directed') ? opts.directed : true
// Label for the graph itself
this._label = undefined
// Defaults to be set when creating a new node
this._defaultNodeLabelFn = constant(undefined)
// Defaults to be set when creating a new edge
this._defaultEdgeLabelFn = constant(undefined)
// v -> label
this._nodes = {}
// v -> edgeObj
this._in = {}
// u -> v -> Number
this._preds = {}
// v -> edgeObj
this._out = {} as Record<string, Record<string, string>>
// v -> w -> Number
this._sucs = {}
// e -> edgeObj
this._edgeObjs = {}
// e -> label
this._edgeLabels = {}
}
/* === Graph functions ========= */
isDirected() {
return this._isDirected
}
setGraph(label) {
this._label = label
return this
}
graph() {
return this._label
}
/* === Node functions ========== */
nodeCount() {
return this._nodeCount
}
nodes() {
return Object.keys(this._nodes)
}
setNode(v, value: any = undefined) {
if (has(this._nodes, v)) {
if (arguments.length > 1) {
this._nodes[v] = value
}
return this
}
this._nodes[v] = arguments.length > 1 ? value : this._defaultNodeLabelFn(v)
this._in[v] = {}
this._preds[v] = {}
this._out[v] = {}
this._sucs[v] = {}
++this._nodeCount
return this
}
setNodes(vs, value) {
vs.forEach((v) => {
if (value !== undefined) {
this.setNode(v, value)
} else {
this.setNode(v)
}
})
return this
}
node(v) {
return this._nodes[v]
}
hasNode(v) {
return has(this._nodes, v)
}
successors(v) {
const sucsV = this._sucs[v]
if (sucsV) {
return Object.keys(sucsV)
}
}
/* === Edge functions ========== */
edgeCount() {
return this._edgeCount
}
edges() {
return Object.values(this._edgeObjs)
}
setEdge(
v: string,
w: string,
value: any = undefined,
name: string | undefined = undefined
) {
v = '' + v
w = '' + w
const e = edgeArgsToId(this._isDirected, v, w, name)
if (has(this._edgeLabels, e)) {
if (value !== undefined) {
this._edgeLabels[e] = value
}
return this
}
// It didn't exist, so we need to create it.
// First ensure the nodes exist.
this.setNode(v)
this.setNode(w)
this._edgeLabels[e] =
value !== undefined ? value : this._defaultEdgeLabelFn(v, w, name)
const edgeObj = edgeArgsToObj(this._isDirected, v, w, name)
// Ensure we add undirected edges in a consistent way.
v = edgeObj.v
w = edgeObj.w
Object.freeze(edgeObj)
this._edgeObjs[e] = edgeObj
incrementOrInitEntry(this._preds[w], v)
incrementOrInitEntry(this._sucs[v], w)
this._in[w][e] = edgeObj
this._out[v][e] = edgeObj
this._edgeCount++
return this
}
edge(v, w, name) {
const e =
arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name)
return this._edgeLabels[e]
}
hasEdge(v, w, name) {
const e =
arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name)
return has(this._edgeLabels, e)
}
removeEdge(v, w, name) {
const e =
arguments.length === 1
? edgeObjToId(this._isDirected, arguments[0])
: edgeArgsToId(this._isDirected, v, w, name)
const edge = this._edgeObjs[e]
if (edge) {
v = edge.v
w = edge.w
delete this._edgeLabels[e]
delete this._edgeObjs[e]
decrementOrRemoveEntry(this._preds[w], v)
decrementOrRemoveEntry(this._sucs[v], w)
delete this._in[w][e]
delete this._out[v][e]
this._edgeCount--
}
return this
}
outEdges(v: string, w: string | undefined = undefined) {
const outV = this._out[v]
if (outV) {
const edges: any = Object.values(outV)
if (w === undefined) {
return edges
}
return edges.filter(function (edge) {
return edge.w === w
})
}
}
}
/** Cycles stuff **/
function tarjan(graph) {
let index = 0
const stack: any[] = []
const visited = {} // node id -> { onStack, lowlink, index }
const results: any[] = []
function dfs(v) {
const entry = (visited[v] = {
onStack: true,
lowlink: index,
index: index++,
})
stack.push(v)
graph.successors(v).forEach(function (w) {
if (!Object.prototype.hasOwnProperty.call(visited, w)) {
dfs(w)
entry.lowlink = Math.min(entry.lowlink, visited[w].lowlink)
} else if (visited[w].onStack) {
entry.lowlink = Math.min(entry.lowlink, visited[w].index)
}
})
if (entry.lowlink === entry.index) {
const cmpt: any[] = []
let w
do {
w = stack.pop()
visited[w].onStack = false
cmpt.push(w)
} while (v !== w)
results.push(cmpt)
}
}
graph.nodes().forEach(function (v) {
if (!Object.prototype.hasOwnProperty.call(visited, v)) {
dfs(v)
}
})
return results
}
export function findCycles(graph): string[][] {
return tarjan(graph).filter(function (cmpt) {
return (
cmpt.length > 1 || (cmpt.length === 1 && graph.hasEdge(cmpt[0], cmpt[0]))
)
})
}

View File

@ -1,10 +1,11 @@
import graphlib from '@dagrejs/graphlib'
import { ASTNode } from './types'
import parsePublicodes from '../parsePublicodes'
import { RuleNode } from '../rule'
import { reduceAST } from './index'
type RulesDependencies = Array<[string, Array<string>]>
type GraphCycles = Array<Array<string>>
type GraphCyclesWithDependencies = Array<RulesDependencies>
import { getChildrenNodes, iterAST } from './index'
import { findCycles, Graph } from './findCycles'
type RulesDependencies = [string, string[]][]
type GraphCycles = string[][]
function buildRulesDependencies(
parsedRules: Record<string, RuleNode>
@ -12,42 +13,56 @@ function buildRulesDependencies(
const uniq = <T>(arr: Array<T>): Array<T> => [...new Set(arr)]
return Object.entries(parsedRules).map(([name, node]) => [
name,
uniq(buildRuleDependancies(node)),
uniq(getDependencies(node)),
])
}
function buildRuleDependancies(rule: RuleNode): Array<string> {
return reduceAST<string[]>(
(acc, node, fn) => {
switch (node.nodeKind) {
case 'replacementRule':
case 'inversion':
case 'une possibilité':
return acc
case 'recalcul':
return node.explanation.amendedSituation.flatMap((s) => fn(s[1]))
case 'reference':
return [...acc, node.dottedName as string]
case 'résoudre référence circulaire':
return []
case 'rule':
// Cycle from parent dependancies are ignored at runtime,
// so we don' detect them statically
return fn(rule.explanation.valeur)
case 'variations':
// a LOT of cycles with replacements... we disactivate them until we see clearer,
if (node.rawNode && typeof node.rawNode === 'string') {
return [...acc, node.rawNode]
}
}
},
[],
rule
)
function getReferenceName(node: ASTNode): string | undefined {
switch (node.nodeKind) {
case 'reference':
return node.dottedName as string
}
}
/**
* Recursively selects the children nodes that have the ability to include a reference
* to a rule.
*/
function getReferencingDescendants(node: ASTNode): ASTNode[] {
return iterAST((node) => {
switch (node.nodeKind) {
case 'replacementRule':
case 'inversion':
case 'une possibilité':
case 'reference':
case 'résoudre référence circulaire':
// "résoudre référence circulaire" is a chained mechanism. When returning `[]` we prevent
// iteration inside of the rule's `valeur`, meaning the rule returns no descendants at all.
return []
case 'recalcul':
return node.explanation.amendedSituation.map(([, astNode]) => astNode)
case 'rule':
return [node.explanation.valeur]
case 'variations':
if (node.visualisationKind === 'replacement') {
return node.explanation
.filter(({ condition }) => condition.isDefault)
.map(({ consequence }) => consequence)
.filter((consequence) => consequence.nodeKind === 'reference')
}
}
return getChildrenNodes(node)
}, node)
}
function getDependencies(node: ASTNode): string[] {
const descendantNodes = Array.from(getReferencingDescendants(node))
const descendantsReferences = descendantNodes
.map(getReferenceName)
.filter((refName): refName is string => refName !== undefined)
return descendantsReferences
}
function buildDependenciesGraph(rulesDeps: RulesDependencies): graphlib.Graph {
const g = new (graphlib as any).Graph()
function buildDependenciesGraph(rulesDeps: RulesDependencies) {
const g = new Graph()
rulesDeps.forEach(([ruleDottedName, dependencies]) => {
dependencies.forEach((depDottedName) => {
g.setEdge(ruleDottedName, depDottedName)
@ -62,40 +77,106 @@ export function cyclesInDependenciesGraph(rawRules: RawRules): GraphCycles {
const parsedRules = parsePublicodes(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = (graphlib as any).alg.findCycles(dependenciesGraph)
const cycles = findCycles(dependenciesGraph)
return cycles.map((c) => c.reverse())
}
/**
* Make the cycle as small as possible.
*/
export function squashCycle(
rulesDependenciesObject: Record<string, string[]>,
cycle: string[]
): string[] {
function* loopFrom(i: number) {
let j = i
while (true) {
yield cycle[j++ % cycle.length]
}
}
const smallCycleStartingAt: string[][] = []
for (let i = 0; i < cycle.length; i++) {
const smallCycle: string[] = []
let previousVertex: string | undefined = undefined
for (const vertex of loopFrom(i)) {
if (previousVertex === undefined) {
smallCycle.push(vertex)
previousVertex = vertex
} else if (rulesDependenciesObject[previousVertex].includes(vertex)) {
if (smallCycle.includes(vertex)) {
smallCycle.splice(0, smallCycle.lastIndexOf(vertex))
break
}
smallCycle.push(vertex)
previousVertex = vertex
}
}
smallCycleStartingAt.push(smallCycle)
}
const smallest = smallCycleStartingAt.reduce((minCycle, someCycle) =>
someCycle.length > minCycle.length ? minCycle : someCycle
)
return smallest
}
/**
* This function is useful so as to print the dependencies at each node of the
* cycle.
* Indeed, the graphlib.findCycles function returns the cycle found using the
* Indeed, the findCycles function returns the cycle found using the
* Tarjan method, which is **not necessarily the smallest cycle**. However, the
* smallest cycle would be the most legibe one
* smallest cycle is more readable.
*/
export function cyclicDependencies(
rawRules: RawRules
): GraphCyclesWithDependencies {
): [GraphCycles, string[]] {
const parsedRules = parsePublicodes(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = (graphlib as any).alg.findCycles(dependenciesGraph)
const rulesDependenciesObject = Object.fromEntries(rulesDependencies)
const cycles = findCycles(dependenciesGraph)
return cycles.map((cycle) => {
const c = cycle.reverse()
const reversedCycles = cycles.map((c) => c.reverse())
const rulesDependenciesObject = Object.fromEntries(
rulesDependencies
) as Record<string, string[]>
const smallCycles = reversedCycles.map((cycle) =>
squashCycle(rulesDependenciesObject, cycle)
)
return [...c, c[0]].reduce((acc, current) => {
const previous = acc.slice(-1)[0]
if (previous && !rulesDependenciesObject[previous].includes(current)) {
return acc
}
return [...acc, current]
}, [])
// .map(name => [
// name,
// rulesDependenciesObject[name].filter(name => c.includes(name))
// ])
})
const printableStronglyConnectedComponents = reversedCycles.map((c, i) =>
printInDotFormat(dependenciesGraph, c, smallCycles[i])
)
return [smallCycles, printableStronglyConnectedComponents]
}
/**
* Is edge in the cycle, in the same order?
*/
const edgeIsInCycle = (cycle: string[], v: string, w: string): boolean => {
for (let i = 0; i < cycle.length + 1; i++) {
if (v === cycle[i] && w === cycle[(i + 1) % cycle.length]) return true
}
return false
}
export function printInDotFormat(
dependenciesGraph: Graph,
cycle: string[],
subCycleToHighlight: string[]
) {
const edgesSet = new Set()
cycle.forEach((vertex) => {
dependenciesGraph
.outEdges(vertex)
.filter(({ w }) => cycle.includes(w))
.forEach(({ v, w }) => {
edgesSet.add(
`"${v}" -> "${w}"` +
(edgeIsInCycle(subCycleToHighlight, v, w) ? ' [color=red]' : '')
)
})
})
return `digraph Cycle {\n\t${[...edgesSet].join(';\n\t')};\n}`
}

View File

@ -2,9 +2,14 @@ import { InternalError } from '../error'
import { TrancheNodes } from '../mecanisms/trancheUtils'
import { ReplacementRule } from '../replacement'
import { RuleNode } from '../rule'
import { ASTNode, NodeKind, TraverseFunction } from './types'
import {
ASTNode,
ASTVisitor,
ASTTransformer,
NodeKind,
TraverseFunction,
} from './types'
type TransformASTFunction = (n: ASTNode) => ASTNode
/**
This function creates a transormation of the AST from on a simpler
callback function `fn`
@ -20,42 +25,68 @@ type TransformASTFunction = (n: ASTNode) => ASTNode
by using the function passed as second argument. The returned value will be the
transformed version of the node.
*/
export function transformAST(
fn: (
node: ASTNode,
updateFn: TransformASTFunction
) => ASTNode | undefined | false
): TransformASTFunction {
function traverseFn(node: ASTNode) {
const updatedNode = fn(node, traverseFn)
export function makeASTTransformer(
fn: (node: ASTNode, transform: ASTTransformer) => ASTNode | undefined | false
): ASTTransformer {
function transform(node: ASTNode): ASTNode {
const updatedNode = fn(node, transform)
if (updatedNode === false) {
return node
}
if (updatedNode === undefined) {
return traverseASTNode(traverseFn, node)
return traverseASTNode(transform, node)
}
return updatedNode
}
return traverseFn
return transform
}
export function makeASTVisitor(
fn: (node: ASTNode, visit: ASTVisitor) => 'continue' | 'stop'
): ASTVisitor {
function visit(node: ASTNode) {
switch (fn(node, visit)) {
case 'continue':
traverseASTNode(transformizedVisit, node)
return
case 'stop':
return
}
}
const transformizedVisit: ASTTransformer = (node) => {
visit(node)
return node
}
return visit
}
// Can be made more flexible with other args like a filter function (ASTNode -> Bool).
export function iterAST(
childrenSelector: (node: ASTNode) => Iterable<ASTNode>,
node: ASTNode
): ASTNode[] {
function* iterate(node: ASTNode): IterableIterator<ASTNode> {
yield node
const selectedSubNodes = childrenSelector(node)
for (const subNode of selectedSubNodes) yield* iterate(subNode)
}
return [...iterate(node)]
}
/**
This function allows to construct a specific value while exploring the AST with
a simple reducing function as argument.
`fn` will be called with the currently reduced value `acc` and the current node of the AST
The outcome of the callback function has an influence on the exploration of the AST :
- `undefined`, the exploration continues further down and all the child are reduced
successively to a single value
- `T`, the reduced value
`reduceFn` : It is possible to specifically use the reduced value of a child
by using the function passed as second argument. The returned value will be the reduced version
of the node
*/
* This function allows to construct a specific value while exploring the AST with
* a simple reducing function as argument.
*
* `fn` will be called with the currently reduced value `acc` and the current node of the AST
*
* If the callback function returns:
* - `undefined`, the exploration continues further down and all the children are reduced
* successively to a single value
* - `T`, the reduced value is returned
*
* `reduceFn` : It is possible to specifically use the reduced value of a child
* by using the function passed as second argument. The returned value will be the reduced version
* of the node
*/
export function reduceAST<T>(
fn: (acc: T, n: ASTNode, reduceFn: (n: ASTNode) => T) => T | undefined,
start: T,
@ -64,14 +95,14 @@ export function reduceAST<T>(
function traverseFn(acc: T, node: ASTNode): T {
const result = fn(acc, node, traverseFn.bind(null, start))
if (result === undefined) {
return gatherNodes(node).reduce(traverseFn, acc)
return getChildrenNodes(node).reduce(traverseFn, acc)
}
return result
}
return traverseFn(start, node)
}
function gatherNodes(node: ASTNode): ASTNode[] {
export function getChildrenNodes(node: ASTNode): ASTNode[] {
const nodes: ASTNode[] = []
traverseASTNode((node) => {
nodes.push(node)
@ -81,7 +112,7 @@ function gatherNodes(node: ASTNode): ASTNode[] {
}
export function traverseParsedRules(
fn: (n: ASTNode) => ASTNode,
fn: ASTTransformer,
parsedRules: Record<string, RuleNode>
): Record<string, RuleNode> {
return Object.fromEntries(
@ -89,7 +120,10 @@ export function traverseParsedRules(
) as Record<string, RuleNode>
}
const traverseASTNode: TraverseFunction<NodeKind> = (fn, node) => {
/**
* Apply a transform function on children. Not recursive.
*/
export const traverseASTNode: TraverseFunction<NodeKind> = (fn, node) => {
switch (node.nodeKind) {
case 'rule':
return traverseRuleNode(fn, node)

View File

@ -85,10 +85,13 @@ export type MecanismNode = Exclude<
ASTNode,
RuleNode | ConstantNode | ReferenceNode
>
export type NodeKind = ASTNode['nodeKind']
export type ASTTransformer = (n: ASTNode) => ASTNode
export type ASTVisitor = (n: ASTNode) => void
export type NodeKind = ASTNode['nodeKind']
export type TraverseFunction<Kind extends NodeKind> = (
fn: (n: ASTNode) => ASTNode,
fn: ASTTransformer,
node: ASTNode & { nodeKind: Kind }
) => ASTNode & { nodeKind: Kind }

View File

@ -40,7 +40,7 @@ export type EvaluationOptions = Partial<{
unit: string
}>
export { reduceAST, transformAST } from './AST/index'
export { reduceAST, makeASTTransformer as transformAST } from './AST/index'
export { Evaluation, Unit } from './AST/types'
export { capitalise0, formatValue } from './format'
export { simplifyNodeUnit } from './nodeUnits'

View File

@ -1,6 +1,6 @@
import yaml from 'yaml'
import { ParsedRules, Logger } from '.'
import { transformAST, traverseParsedRules } from './AST'
import { makeASTTransformer, traverseParsedRules } from './AST'
import parse from './parse'
import { getReplacements, inlineReplacements } from './replacement'
import { Rule, RuleNode } from './rule'
@ -105,7 +105,7 @@ function transpileRef(object: Record<string, any> | string | Array<any>) {
}
export const disambiguateReference = (parsedRules: Record<string, RuleNode>) =>
transformAST((node) => {
makeASTTransformer((node) => {
if (node.nodeKind === 'reference') {
const dottedName = disambiguateRuleReference(
parsedRules,

View File

@ -1,5 +1,5 @@
import { Logger } from '.'
import { transformAST } from './AST'
import { makeASTTransformer } from './AST'
import { ASTNode } from './AST/types'
import { InternalError, warning } from './error'
import { defaultNode } from './evaluation'
@ -25,7 +25,7 @@ export type ReplacementRule = {
//
// The implementation works by first attributing an identifier for each
// replacementRule. We then use this identifier to create a cache key that
// represent the combinaison of applicables replacements for a given reference.
// represents the combinaison of applicables replacements for a given reference.
// For example if replacements 12, 13 et 643 are applicable we use the key
// `12-13-643` as the cache identifier in the `inlineReplacements` function.
let remplacementRuleId = 0
@ -104,31 +104,31 @@ export function inlineReplacements(
replacements: Record<string, Array<ReplacementRule>>,
logger: Logger
): (n: ASTNode) => ASTNode {
return transformAST((n, fn) => {
return makeASTTransformer((node, transform) => {
if (
n.nodeKind === 'replacementRule' ||
n.nodeKind === 'inversion' ||
n.nodeKind === 'une possibilité'
node.nodeKind === 'replacementRule' ||
node.nodeKind === 'inversion' ||
node.nodeKind === 'une possibilité'
) {
return false
}
if (n.nodeKind === 'recalcul') {
if (node.nodeKind === 'recalcul') {
// We don't replace references in recalcul keys
return {
...n,
...node,
explanation: {
recalcul: fn(n.explanation.recalcul),
amendedSituation: n.explanation.amendedSituation.map(
([name, value]) => [name, fn(value)]
recalcul: transform(node.explanation.recalcul),
amendedSituation: node.explanation.amendedSituation.map(
([name, value]) => [name, transform(value)]
),
},
}
}
if (n.nodeKind === 'reference') {
if (!n.dottedName) {
throw new InternalError(n)
if (node.nodeKind === 'reference') {
if (!node.dottedName) {
throw new InternalError(node)
}
return replace(n, replacements[n.dottedName] ?? [], logger)
return replace(node, replacements[node.dottedName] ?? [], logger)
}
})
}

View File

@ -137,7 +137,9 @@ registerEvaluationFunction('rule', function evaluate(node) {
if (
this.cache._meta.evaluationRuleStack.filter(
(dottedName) => dottedName === node.dottedName
).length > 15 // I don't know why this magic number, but below, cycle are detected "too early", which leads to blank value in brut-net simulator
).length > 15
// I don't know why this magic number, but below, cycle are
// detected "too early", which leads to blank value in brut-net simulator
) {
warning(
this.options.logger,

View File

@ -1,8 +0,0 @@
declare module '@dagrejs/graphlib' {
export interface Graph {
setEdge(n1: string, n2: string): void
}
export type alg = {
findCycles: (g: Graph) => Array<Array<string>>
}
}

View File

@ -27,7 +27,7 @@ describe('Cyclic dependencies detectron 3000 ™', () => {
expect(cycles).to.deep.equal([['a', 'b', 'c', 'd']])
})
it('should not detect formule cycles due to parent dependancy', () => {
it('should not detect formule cycles due to parent dependency', () => {
const rules = dedent`
a:
formule: b + 1
@ -38,17 +38,29 @@ describe('Cyclic dependencies detectron 3000 ™', () => {
expect(cycles).to.deep.equal([])
})
it('should not detect cycles due to replacement', () => {
it('should not detect cycles due to replacements', () => {
const rules = dedent`
a:
formule: b + 1
a . b:
formule: 3
a . c:
a . c:
remplace: b
formule: a
`
const cycles = cyclesInDependenciesGraph(rules)
expect(cycles).to.deep.equal([['a', 'a . c']])
expect(cycles).to.deep.equal([])
})
it('should not detect cycles when résoudre référence circulaire', () => {
const rules = dedent`
fx:
200 - x
x:
résoudre la référence circulaire: oui
valeur: fx
`
const cycles = cyclesInDependenciesGraph(rules)
expect(cycles).to.deep.equal([])
})
})

View File

@ -113,8 +113,8 @@ exemple5:
par: 10
- règle: cotisations . maladie
par: 0
remplacements :
remplacements:
formule: cotisations
exemples:
- nom: sans boucle infinie si il n'y a pas de dépendances cycliques
@ -125,8 +125,8 @@ remplacements :
situation:
exemple2: oui
valeur attendue: 250
- nom: avec plusieurs remplacements existant pour une même variables
# ici, le remplacement de l'exemple 2 doit être effectué car plus précis que celui de l'exemple 1
- nom: avec plusieurs remplacements existant pour une même variables
# ici, le remplacement de l'exemple 2 doit être effectué car plus précis que celui de l'exemple 1
situation:
exemple1: oui
exemple2: oui
@ -149,8 +149,6 @@ remplacements :
exemple5: oui
valeur attendue: 110
A:
formule: 1
B:
@ -181,19 +179,18 @@ remplacement non applicable car branche desactivée:
exemples:
- valeur attendue: 1
# Remplacement non effectué dans la formule du remplacement
# Remplacement effectué dans la bonne variable
espace: oui
espace . valeur:
formule: 20
espace . remplacement:
remplace: valeur
formule: valeur + 10
test remplacement non effectué dans la formule du remplacement:
test remplacement effectué dans la variable à remplacer:
formule: espace . valeur
exemples:
- valeur attendue: 30
frais de repas:
formule: 5 €/repas

View File

@ -1666,13 +1666,6 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@dagrejs/graphlib@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.1.4.tgz#86c70e4f073844a2f4ada254c8c7b88a6bdacdb1"
integrity sha512-QCg9sL4uhjn468FDEsb/S9hS2xUZSrv/+dApb1Ze5VKO96pTXKNJZ6MGhIpgWkc1TVhbVGH9/7rq/Mf8/jWicw==
dependencies:
lodash "^4.11.1"
"@emotion/is-prop-valid@^0.8.8":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
@ -9073,7 +9066,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.0.1, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.4:
lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.4:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==