⚙️ 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/graphlibpull/1136/head
parent
960fda08e6
commit
343ca00a27
|
@ -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
|
||||
})
|
||||
})
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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]])
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { cyclicDependencies } from './graph'
|
||||
|
||||
export default { cyclicDependencies }
|
|
@ -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)]
|
||||
)
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -12,7 +12,7 @@ type MecanismRoundProps = {
|
|||
explanation: ArrondiExplanation
|
||||
}
|
||||
|
||||
type ArrondiExplanation = {
|
||||
export type ArrondiExplanation = {
|
||||
value: EvaluatedNode<string, number>
|
||||
decimals: EvaluatedNode<string, number>
|
||||
}
|
||||
|
|
|
@ -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']])
|
||||
})
|
||||
})
|
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue