⚙️ Detect cycles in parsed rules

Build a dependencies graph and detect cycles:
* Types and guards for nodes of the ParsedRules AST
* Simple visitor framework for the nodes and their `formule` sub-nodes
* Build a directed graph for dependencies using @dagrejs/graphlib
pull/1136/head
Alexandre Hajjar 2020-10-08 11:29:07 +02:00
parent 960fda08e6
commit 343ca00a27
10 changed files with 1055 additions and 2 deletions

View File

@ -0,0 +1,27 @@
import { expect } from 'chai'
import { cyclesLib } from 'publicodes'
import rules from '../source/rules'
describe('DottedNames graph', () => {
it("shouldn't have cycles", () => {
let cyclesDependencies = cyclesLib.cyclicDependencies(rules)
expect(
cyclesDependencies.reverse(),
`\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↘️ is each of the dependencies of this node.\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⬇ ')
)
.join('\n\t- ')}\n\n`
).to.be.an('array').that.is.empty
})
})

View File

@ -18,6 +18,9 @@
"dist/images"
],
"private": false,
"devDependencies": {
"@dagrejs/graphlib": "^2.1.4"
},
"dependencies": {
"classnames": "^2.2.6",
"focus-trap-react": "^3.1.2",

View File

@ -0,0 +1,535 @@
/**
* Note: all here is strictly based on duck typing.
* We don't exepect the parent rule to explain the type of the contained formula, for example.
*/
import * as R from 'ramda'
import { ParsedRule } from '../types'
import { ArrondiExplanation } from '../mecanisms/arrondi'
export type OnOff = 'oui' | 'non'
export function isOnOff(a: string): a is OnOff {
return a === 'oui' || a === 'non'
}
// Note: to build type-guards, we would need to have a `isNames` guard. That's
// pretty cumbersome, so for now we rely on this.
export type WannabeDottedName = string
export function isWannabeDottedName(a: string): a is WannabeDottedName {
return typeof a === 'string'
}
export type ASTNode = { [_: string]: any | undefined }
export type RuleNode<Names extends string> = ASTNode & ParsedRule<Names>
export type RuleProp = ASTNode & {
category: 'ruleProp'
rulePropType: string
}
export function isRuleProp(node: ASTNode): node is RuleProp {
return (
(node as RuleProp).category === 'ruleProp' &&
typeof (node as RuleProp).rulePropType === 'string'
)
}
export type Formule<Names extends string> = RuleProp & {
name: 'formule'
rulePropType: 'formula'
explanation: FormuleExplanation<Names>
}
export function isFormule<Names extends string>(
node: ASTNode
): node is Formule<Names> {
const formule = node as Formule<Names>
return (
isRuleProp(node) &&
formule.name === 'formule' &&
formule.rulePropType === 'formula' &&
isFormuleExplanation<Names>(formule.explanation)
)
}
export type FormuleExplanation<Names extends string> =
| Value
| Operation
| Possibilities
| Possibilities2
| Reference<Names>
| AnyMechanism<Names>
export function isFormuleExplanation<Names extends string>(
node: ASTNode
): node is FormuleExplanation<Names> {
return (
isValue(node) ||
isOperation(node) ||
isReference(node) ||
isPossibilities(node) ||
isPossibilities2(node) ||
isAnyMechanism<Names>(node)
)
}
export type Value = ASTNode & {
nodeValue: number | string | boolean
}
export function isValue(node: ASTNode): node is Value {
const value = node as Value
return (
typeof value.nodeValue === 'string' ||
typeof value.nodeValue === 'number' ||
typeof value.nodeValue === 'boolean'
)
}
export type Operation = ASTNode & {
operationType: 'comparison' | 'calculation'
explanation: Array<ASTNode>
}
export function isOperation(node: ASTNode): node is Operation {
return R.includes((node as Operation).operationType, [
'comparison',
'calculation'
])
}
export type Possibilities = ASTNode & {
possibilités: Array<string>
'choix obligatoire'?: OnOff
'une possibilité': OnOff
}
export function isPossibilities(node: ASTNode): node is Possibilities {
const possibilities = node as Possibilities
return (
possibilities.possibilités instanceof Array &&
possibilities.possibilités.every(it => typeof it === 'string') &&
(possibilities['choix obligatoire'] === undefined ||
isOnOff(possibilities['choix obligatoire'])) &&
isOnOff(possibilities['une possibilité'])
)
}
export type Possibilities2 = ASTNode & {
[index: number]: string // short dotted name
'choix obligatoire'?: OnOff
'une possibilité': OnOff
}
export function isPossibilities2(node: ASTNode): node is Possibilities2 {
const possibilities2 = node as Possibilities2
return (
Object.entries(possibilities2).every(
([k, v]) => isNaN(parseInt(k, 10)) || typeof v === 'string'
) &&
(possibilities2['choix obligatoire'] === undefined ||
isOnOff(possibilities2['choix obligatoire'])) &&
isOnOff(possibilities2['une possibilité'])
)
}
export type Reference<Names extends string> = ASTNode & {
category: 'reference'
name: Names
partialReference: Names
dottedName: Names
}
export function isReference<Names extends string>(
node: ASTNode
): node is Reference<Names> {
const reference = node as Reference<Names>
return (
reference.category === 'reference' &&
isWannabeDottedName(reference.name) &&
isWannabeDottedName(reference.partialReference) &&
isWannabeDottedName(reference.dottedName)
)
}
export type AbstractMechanism = ASTNode & {
category: 'mecanism'
name: string
}
export function isAbstractMechanism(node: ASTNode): node is AbstractMechanism {
return (
(node as AbstractMechanism).category === 'mecanism' &&
typeof (node as AbstractMechanism).name === 'string'
)
}
export type RecalculMech<Names extends string> = AbstractMechanism & {
explanation: {
recalcul: Reference<Names>
amendedSituation: Record<Names, Reference<Names>>
}
}
export function isRecalculMech<Names extends string>(
node: ASTNode
): node is RecalculMech<Names> {
const recalculMech = node as RecalculMech<Names>
const isReferenceSpec = isReference as (
node: ASTNode
) => node is Reference<Names>
return (
typeof recalculMech.explanation === 'object' &&
typeof recalculMech.explanation.recalcul === 'object' &&
isReferenceSpec(recalculMech.explanation.recalcul as ASTNode) &&
typeof recalculMech.explanation.amendedSituation === 'object'
)
}
export type EncadrementMech = AbstractMechanism & {
name: 'encadrement'
explanation: {
valeur: ASTNode
plafond: ASTNode
plancher: ASTNode
}
}
export function isEncadrementMech(node: ASTNode): node is EncadrementMech {
const encadrementMech = node as EncadrementMech
return (
isAbstractMechanism(encadrementMech) &&
encadrementMech.name == 'encadrement' &&
typeof encadrementMech.explanation === 'object' &&
encadrementMech.explanation.valeur !== undefined &&
encadrementMech.explanation.plafond !== undefined &&
encadrementMech.explanation.plancher !== undefined
)
}
export type SommeMech = AbstractMechanism & {
name: 'somme'
explanation: Array<ASTNode>
}
export function isSommeMech(node: ASTNode): node is SommeMech {
const sommeMech = node as SommeMech
return (
isAbstractMechanism(sommeMech) &&
sommeMech.name === 'somme' &&
sommeMech.explanation instanceof Array
)
}
export type ProduitMech = AbstractMechanism & {
name: 'produit'
explanation: {
assiette: ASTNode
plafond: ASTNode
facteur: ASTNode
taux: ASTNode
}
}
export function isProduitMech(node: ASTNode): node is ProduitMech {
const produitMech = node as ProduitMech
return (
isAbstractMechanism(produitMech) &&
produitMech.name === 'produit' &&
typeof produitMech.explanation === 'object' &&
typeof produitMech.explanation.assiette === 'object' &&
typeof produitMech.explanation.plafond === 'object' &&
typeof produitMech.explanation.facteur === 'object' &&
typeof produitMech.explanation.taux === 'object'
)
}
export type VariationsMech = AbstractMechanism & {
name: 'variations'
explanation: {
condition: ASTNode
consequence: ASTNode
}[]
}
export function isVariationsMech(node: ASTNode): node is VariationsMech {
const variationsMech = node as VariationsMech
return (
isAbstractMechanism(variationsMech) &&
variationsMech.name === 'variations' &&
variationsMech.explanation instanceof Array &&
variationsMech.explanation.every(
variation =>
typeof variation === 'object' &&
variation.condition !== undefined &&
variation.consequence !== undefined
)
)
}
export type AllegementMech = AbstractMechanism & {
name: 'allègement'
explanation: {
abattement: ASTNode
assiette: ASTNode
plafond: ASTNode
}
}
export function isAllegementMech(node: ASTNode): node is AllegementMech {
const allegementMech = node as AllegementMech
return (
isAbstractMechanism(allegementMech) &&
allegementMech.name === 'allègement' &&
typeof allegementMech.explanation === 'object' &&
allegementMech.explanation.abattement !== undefined &&
allegementMech.explanation.assiette !== undefined &&
allegementMech.explanation.plafond !== undefined
)
}
export type BaremeMech = AbstractMechanism & {
name: 'barème'
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: {
plafond: ASTNode
taux: ASTNode
}[]
}
}
export function isBaremeMech(node: ASTNode): node is BaremeMech {
const baremeMech = node as BaremeMech
return (
isAbstractMechanism(baremeMech) &&
baremeMech.name === 'barème' &&
typeof baremeMech.explanation === 'object' &&
baremeMech.explanation.assiette !== undefined &&
baremeMech.explanation.multiplicateur !== undefined &&
baremeMech.explanation.tranches instanceof Array &&
baremeMech.explanation.tranches.every(
tranche =>
typeof tranche === 'object' &&
tranche.plafond !== undefined &&
tranche.taux !== undefined
)
)
}
export type InversionNumMech<Names extends string> = AbstractMechanism & {
name: 'inversion numérique'
explanation: {
inversionCandidates: Array<Reference<Names>>
}
}
export function isInversionNumMech<Names extends string>(
node: ASTNode
): node is InversionNumMech<Names> {
const inversionNumMech = node as InversionNumMech<Names>
const isReferenceSpec = isReference as (
node: ASTNode
) => node is Reference<Names>
return (
isAbstractMechanism(inversionNumMech) &&
inversionNumMech.name === 'inversion numérique' &&
typeof inversionNumMech.explanation === 'object' &&
inversionNumMech.explanation.inversionCandidates instanceof Array &&
inversionNumMech.explanation.inversionCandidates.every(isReferenceSpec)
)
}
export type ArrondiMech = AbstractMechanism & {
name: 'arrondi'
explanation: Record<keyof ArrondiExplanation, ASTNode>
}
export function isArrondiMech(node: ASTNode): node is ArrondiMech {
const arrondiMech = node as ArrondiMech
return (
isAbstractMechanism(arrondiMech) &&
arrondiMech.name === 'arrondi' &&
typeof arrondiMech.explanation === 'object' &&
arrondiMech.explanation.decimals !== undefined &&
arrondiMech.explanation.value !== undefined
)
}
export type MaxMech = AbstractMechanism & {
name: 'le maximum de'
explanation: Array<ASTNode>
}
export function isMaxMech(node: ASTNode): node is MaxMech {
const maxMech = node as MaxMech
return (
isAbstractMechanism(maxMech) &&
maxMech.name === 'le maximum de' &&
maxMech.explanation instanceof Array
)
}
export type MinMech = AbstractMechanism & {
name: 'le minimum de'
explanation: Array<ASTNode>
}
export function isMinMech(node: ASTNode): node is MinMech {
const minMech = node as MinMech
return (
isAbstractMechanism(minMech) &&
minMech.name === 'le minimum de' &&
minMech.explanation instanceof Array
)
}
export type ComposantesMech = AbstractMechanism & {
name: 'composantes'
explanation: Array<ASTNode>
}
export function isComposantesMech(node: ASTNode): node is ComposantesMech {
const composantesMech = node as ComposantesMech
return (
isAbstractMechanism(composantesMech) &&
composantesMech.name === 'composantes' &&
composantesMech.explanation instanceof Array
)
}
export type UneConditionsMech = AbstractMechanism & {
name: 'une de ces conditions'
explanation: Array<ASTNode>
}
export function isUneConditionsMech(node: ASTNode): node is UneConditionsMech {
const uneConditionsMech = node as UneConditionsMech
return (
isAbstractMechanism(uneConditionsMech) &&
uneConditionsMech.name === 'une de ces conditions' &&
uneConditionsMech.explanation instanceof Array
)
}
export type ToutesConditionsMech = AbstractMechanism & {
name: 'toutes ces conditions'
explanation: Array<ASTNode>
}
export function isToutesConditionsMech(
node: ASTNode
): node is ToutesConditionsMech {
const toutesConditionsMech = node as ToutesConditionsMech
return (
isAbstractMechanism(toutesConditionsMech) &&
toutesConditionsMech.name === 'toutes ces conditions' &&
toutesConditionsMech.explanation instanceof Array
)
}
export type SyncMech = AbstractMechanism & {
name: 'synchronisation'
API: any
}
export function isSyncMech(node: ASTNode): node is SyncMech {
const syncMech = node as SyncMech
return isAbstractMechanism(syncMech) && syncMech.name === 'synchronisation'
}
export type GrilleMech = AbstractMechanism & {
name: 'grille'
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: {
montant: ASTNode
plafond: ASTNode
}[]
}
}
export function isGrilleMech(node: ASTNode): node is GrilleMech {
const grilleMech = node as GrilleMech
return (
isAbstractMechanism(grilleMech) &&
grilleMech.name === 'grille' &&
typeof grilleMech.explanation === 'object' &&
grilleMech.explanation.assiette !== undefined &&
grilleMech.explanation.multiplicateur !== undefined &&
grilleMech.explanation.tranches instanceof Array &&
grilleMech.explanation.tranches.every(
tranche =>
typeof tranche === 'object' &&
tranche.montant !== undefined &&
tranche.plafond !== undefined
)
)
}
export type TauxProgMech = AbstractMechanism & {
name: 'taux progressif'
explanation: {
assiette: ASTNode
multiplicateur: ASTNode
tranches: {
plafond: ASTNode
taux: ASTNode
}[]
}
}
export function isTauxProgMech(node: ASTNode): node is TauxProgMech {
const tauxProgMech = node as TauxProgMech
return (
isAbstractMechanism(tauxProgMech) &&
tauxProgMech.name === 'taux progressif' &&
typeof tauxProgMech.explanation === 'object' &&
tauxProgMech.explanation.assiette !== undefined &&
tauxProgMech.explanation.multiplicateur !== undefined &&
tauxProgMech.explanation.tranches instanceof Array &&
tauxProgMech.explanation.tranches.every(
tranche =>
typeof tranche === 'object' &&
tranche.plafond !== undefined &&
tranche.taux !== undefined
)
)
}
export type DureeMech = AbstractMechanism & {
name: 'Durée'
explanation: {
depuis: ASTNode
"jusqu'à": ASTNode
}
}
export function isDureeMech(node: ASTNode): node is DureeMech {
const dureeMech = node as DureeMech
return (
isAbstractMechanism(dureeMech) &&
dureeMech.name === 'Durée' &&
typeof dureeMech.explanation === 'object' &&
dureeMech.explanation.depuis !== undefined &&
dureeMech.explanation["jusqu'à"] !== undefined
)
}
export type AnyMechanism<Names extends string> =
| RecalculMech<Names>
| EncadrementMech
| SommeMech
| ProduitMech
| VariationsMech
| AllegementMech
| BaremeMech
| InversionNumMech<Names>
| ArrondiMech
| MaxMech
| MinMech
| ComposantesMech
| UneConditionsMech
| ToutesConditionsMech
| SyncMech
| GrilleMech
| TauxProgMech
| DureeMech
export function isAnyMechanism<Names extends string>(
node: ASTNode
): node is AnyMechanism<Names> {
return (
isRecalculMech<Names>(node) ||
isEncadrementMech(node) ||
isSommeMech(node) ||
isProduitMech(node) ||
isVariationsMech(node) ||
isAllegementMech(node) ||
isBaremeMech(node) ||
isInversionNumMech<Names>(node) ||
isArrondiMech(node) ||
isMaxMech(node) ||
isMinMech(node) ||
isComposantesMech(node) ||
isUneConditionsMech(node) ||
isToutesConditionsMech(node) ||
isSyncMech(node) ||
isGrilleMech(node) ||
isTauxProgMech(node) ||
isDureeMech(node)
)
}

View File

@ -0,0 +1,62 @@
import * as R from 'ramda'
import graphlib from '@dagrejs/graphlib'
import parseRules from '../parseRules'
import { Rules } from '../types'
import {
buildRulesDependencies,
RuleDependencies,
RulesDependencies
} from './rulesDependencies'
type GraphNodeRepr<Names extends string> = Names
type GraphCycles<Names extends string> = Array<Array<GraphNodeRepr<Names>>>
type GraphCyclesWithDependencies<Names extends string> = Array<
Array<[GraphNodeRepr<Names>, RuleDependencies<Names>]>
>
function buildDependenciesGraph<Names extends string>(
rulesDeps: RulesDependencies<Names>
): graphlib.Graph {
const g = new graphlib.Graph()
rulesDeps.forEach(([ruleDottedName, dependencies]) => {
dependencies.forEach(depDottedName => {
g.setEdge(ruleDottedName, depDottedName)
})
})
return g
}
export function cyclesInDependenciesGraph<Names extends string>(
rawRules: Rules<Names> | string
): GraphCycles<Names> {
const parsedRules = parseRules(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = graphlib.alg.findCycles(dependenciesGraph)
return cycles
}
/**
* 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
* Tarjan method, which is **not necessarily the smallest cycle**. However, the
* smallest cycle would be the most legibe one
*/
export function cyclicDependencies<Names extends string>(
rawRules: Rules<Names> | string
): GraphCyclesWithDependencies<Names> {
const parsedRules = parseRules(rawRules)
const rulesDependencies = buildRulesDependencies(parsedRules)
const dependenciesGraph = buildDependenciesGraph(rulesDependencies)
const cycles = graphlib.alg.findCycles(dependenciesGraph)
const rulesDependenciesObject = R.fromPairs(rulesDependencies)
return cycles.map(cycle =>
cycle.map(ruleName => [ruleName, rulesDependenciesObject[ruleName]])
)
}

View File

@ -0,0 +1,3 @@
import { cyclicDependencies } from './graph'
export default { cyclicDependencies }

View File

@ -0,0 +1,386 @@
import * as R from 'ramda'
import { ParsedRules } from '../types'
import * as ASTTypes from './ASTTypes'
export type RuleDependencies<Names extends string> = Array<Names>
export type RulesDependencies<Names extends string> = Array<
[Names, RuleDependencies<Names>]
>
export function ruleDepsOfNode<Names extends string>(
ruleName: Names,
node: ASTTypes.ASTNode
): RuleDependencies<Names> {
function ruleDepsOfFormule(
formule: ASTTypes.Formule<Names>
): RuleDependencies<Names> {
return ruleDepsOfNode(ruleName, formule.explanation)
}
function ruleDepsOfValue(value: ASTTypes.Value): RuleDependencies<Names> {
return []
}
function ruleDepsOfOperation(
operation: ASTTypes.Operation
): RuleDependencies<Names> {
return operation.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
}
function ruleDepsOfPossibilities(
possibilities: ASTTypes.Possibilities
): RuleDependencies<Names> {
return []
}
function ruleDepsOfPossibilities2(
possibilities: ASTTypes.Possibilities2
): RuleDependencies<Names> {
return []
}
function ruleDepsOfReference(
reference: ASTTypes.Reference<Names>
): RuleDependencies<Names> {
return [reference.dottedName]
}
function ruleDepsOfRecalculMech(
recalculMech: ASTTypes.RecalculMech<Names>
): RuleDependencies<Names> {
const ruleReference = recalculMech.explanation.recalcul.partialReference
return ruleReference === ruleName ? [] : [ruleReference]
}
function ruleDepsOfEncadrementMech(
encadrementMech: ASTTypes.EncadrementMech
): RuleDependencies<Names> {
const result = [
encadrementMech.explanation.plafond,
encadrementMech.explanation.plancher,
encadrementMech.explanation.valeur
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfSommeMech(
sommeMech: ASTTypes.SommeMech
): RuleDependencies<Names> {
const result = sommeMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfProduitMech(
produitMech: ASTTypes.ProduitMech
): RuleDependencies<Names> {
const result = [
produitMech.explanation.assiette,
produitMech.explanation.plafond,
produitMech.explanation.facteur,
produitMech.explanation.taux
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfVariationsMech(
variationsMech: ASTTypes.VariationsMech
): RuleDependencies<Names> {
function ruleOfVariation({
condition,
consequence
}: {
condition: ASTTypes.ASTNode
consequence: ASTTypes.ASTNode
}): RuleDependencies<Names> {
return R.concat(
ruleDepsOfNode<Names>(ruleName, condition),
ruleDepsOfNode<Names>(ruleName, consequence)
)
}
const result = variationsMech.explanation.flatMap(ruleOfVariation)
return result
}
function ruleDepsOfAllegementMech(
allegementMech: ASTTypes.AllegementMech
): RuleDependencies<Names> {
const subNodes = [
allegementMech.explanation.abattement,
allegementMech.explanation.assiette,
allegementMech.explanation.plafond
]
const result = subNodes.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfBaremeMech(
baremeMech: ASTTypes.BaremeMech
): RuleDependencies<Names> {
const tranchesNodes = baremeMech.explanation.tranches.flatMap(
({ plafond, taux }) => [plafond, taux]
)
const result = R.concat(
[baremeMech.explanation.assiette, baremeMech.explanation.multiplicateur],
tranchesNodes
).flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
/**
* Returns 0 dependency for _inversion numérique_ as it's not creating a logical dependency.
*/
function ruleDepsOfInversionNumMech(
inversionNumMech: ASTTypes.InversionNumMech<Names>
): RuleDependencies<Names> {
return []
}
function ruleDepsOfArrondiMech(
arrondiMech: ASTTypes.ArrondiMech
): RuleDependencies<Names> {
const result = [
arrondiMech.explanation.decimals,
arrondiMech.explanation.value
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfMaxMech(
maxMech: ASTTypes.MaxMech
): RuleDependencies<Names> {
const result = maxMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfMinMech(
minMech: ASTTypes.MinMech
): RuleDependencies<Names> {
const result = minMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfComposantesMech(
composantesMech: ASTTypes.ComposantesMech
): RuleDependencies<Names> {
const result = composantesMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfUneConditionsMech(
uneConditionsMech: ASTTypes.UneConditionsMech
): RuleDependencies<Names> {
const result = uneConditionsMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfToutesConditionsMech(
toutesConditionsMech: ASTTypes.ToutesConditionsMech
): RuleDependencies<Names> {
const result = toutesConditionsMech.explanation.flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfSyncMech(_: ASTTypes.SyncMech): RuleDependencies<Names> {
return []
}
function ruleDepsOfGrilleMech(
grilleMech: ASTTypes.GrilleMech
): RuleDependencies<Names> {
const tranchesNodes = grilleMech.explanation.tranches.flatMap(
({ montant, plafond }) => [montant, plafond]
)
const result = R.concat(
[grilleMech.explanation.assiette, grilleMech.explanation.multiplicateur],
tranchesNodes
).flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfTauxProgMech(
tauxProgMech: ASTTypes.TauxProgMech
): RuleDependencies<Names> {
const tranchesNodes = tauxProgMech.explanation.tranches.flatMap(
({ plafond, taux }) => [plafond, taux]
)
const result = R.concat(
[
tauxProgMech.explanation.assiette,
tauxProgMech.explanation.multiplicateur
],
tranchesNodes
).flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
function ruleDepsOfDureeMech(
dureeMech: ASTTypes.DureeMech
): RuleDependencies<Names> {
const result = [
dureeMech.explanation.depuis,
dureeMech.explanation["jusqu'à"]
].flatMap(
R.partial<Names, ASTTypes.ASTNode, RuleDependencies<Names>>(
ruleDepsOfNode,
[ruleName]
)
)
return result
}
let result
if (ASTTypes.isFormule<Names>(node)) {
result = ruleDepsOfFormule(node)
} else if (ASTTypes.isValue(node)) {
result = ruleDepsOfValue(node)
} else if (ASTTypes.isOperation(node)) {
result = ruleDepsOfOperation(node)
} else if (ASTTypes.isReference<Names>(node)) {
result = ruleDepsOfReference(node)
} else if (ASTTypes.isPossibilities(node)) {
result = ruleDepsOfPossibilities(node)
} else if (ASTTypes.isPossibilities2(node)) {
result = ruleDepsOfPossibilities2(node)
} else if (ASTTypes.isRecalculMech<Names>(node)) {
result = ruleDepsOfRecalculMech(node)
} else if (ASTTypes.isEncadrementMech(node)) {
result = ruleDepsOfEncadrementMech(node)
} else if (ASTTypes.isSommeMech(node)) {
result = ruleDepsOfSommeMech(node)
} else if (ASTTypes.isProduitMech(node)) {
result = ruleDepsOfProduitMech(node)
} else if (ASTTypes.isVariationsMech(node)) {
result = ruleDepsOfVariationsMech(node)
} else if (ASTTypes.isAllegementMech(node)) {
result = ruleDepsOfAllegementMech(node)
} else if (ASTTypes.isBaremeMech(node)) {
result = ruleDepsOfBaremeMech(node)
} else if (ASTTypes.isInversionNumMech<Names>(node)) {
result = ruleDepsOfInversionNumMech(node)
} else if (ASTTypes.isArrondiMech(node)) {
result = ruleDepsOfArrondiMech(node)
} else if (ASTTypes.isMaxMech(node)) {
result = ruleDepsOfMaxMech(node)
} else if (ASTTypes.isMinMech(node)) {
result = ruleDepsOfMinMech(node)
} else if (ASTTypes.isComposantesMech(node)) {
result = ruleDepsOfComposantesMech(node)
} else if (ASTTypes.isUneConditionsMech(node)) {
result = ruleDepsOfUneConditionsMech(node)
} else if (ASTTypes.isToutesConditionsMech(node)) {
result = ruleDepsOfToutesConditionsMech(node)
} else if (ASTTypes.isSyncMech(node)) {
result = ruleDepsOfSyncMech(node)
} else if (ASTTypes.isGrilleMech(node)) {
result = ruleDepsOfGrilleMech(node)
} else if (ASTTypes.isTauxProgMech(node)) {
result = ruleDepsOfTauxProgMech(node)
} else if (ASTTypes.isDureeMech(node)) {
result = ruleDepsOfDureeMech(node)
}
if (result === undefined) {
throw new Error(
`This node doesn't have a visitor method defined: ${node.name}`
)
}
return result
}
function ruleDepsOfRuleNode<Names extends string>(
ruleNode: ASTTypes.RuleNode<Names>
): RuleDependencies<Names> {
return ruleNode.formule === undefined
? []
: ruleDepsOfNode(ruleNode.dottedName, ruleNode.formule)
}
export function buildRulesDependencies<Names extends string>(
parsedRules: ParsedRules<Names>
): RulesDependencies<Names> {
// This stringPairs thing is necessary because `toPairs` is strictly considering that
// object keys are strings (same for `Object.entries`). Maybe we should build our own
// `toPairs`?
const stringPairs: Array<[string, ASTTypes.RuleNode<Names>]> = Object.entries(
parsedRules
)
const pairs: Array<[Names, ASTTypes.RuleNode<Names>]> = stringPairs as Array<
[Names, ASTTypes.RuleNode<Names>]
>
return pairs.map(
([dottedName, ruleNode]: [Names, ASTTypes.RuleNode<Names>]): [
Names,
RuleDependencies<Names>
] => [dottedName, ruleDepsOfRuleNode<Names>(ruleNode)]
)
}

View File

@ -37,6 +37,7 @@ export type EvaluationOptions = Partial<{
export * from './components'
export { formatValue, serializeValue } from './format'
export { default as translateRules } from './translateRules'
export { default as cyclesLib } from './cyclesLib/index'
export * from './types'
export { parseRules }
export { utils }

View File

@ -12,7 +12,7 @@ type MecanismRoundProps = {
explanation: ArrondiExplanation
}
type ArrondiExplanation = {
export type ArrondiExplanation = {
value: EvaluatedNode<string, number>
decimals: EvaluatedNode<string, number>
}

View File

@ -0,0 +1,29 @@
import { expect } from 'chai'
import dedent from 'dedent-js'
import { cyclesInDependenciesGraph } from '../source/cyclesLib/graph'
describe('Cyclic dependencies detectron 3000 ™', () => {
it('should detect the trivial formule cycle', () => {
const rules = dedent`
a:
formule: a + 1
`
const cycles = cyclesInDependenciesGraph(rules)
expect(cycles).to.deep.equal([['a']])
})
it('should detect nested and parallel formule cycles', () => {
const rules = dedent`
a:
formule: b + 1
b:
formule: c + d + 1
c:
formule: a + 1
d:
formule: b + 1
`
const cycles = cyclesInDependenciesGraph(rules)
expect(cycles).to.deep.equal([['d', 'c', 'b', 'a']])
})
})

View File

@ -965,6 +965,13 @@
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"
@ -7853,7 +7860,7 @@ lodash@4.17.15:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, 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.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, 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==