1
0
Fork 0
mirror of https://github.com/betagouv/mon-entreprise synced 2025-02-09 22:45:01 +00:00
mon-entreprise/publicodes/source/parseReference.ts
Maxime Quandalle 973f12ab46 ⚙️ Évalue la situation dans l'ordre topologique
Nous utilisions jusqu'à présent le code suivant pour évaluer la situation:

> mapObjIndexed(value => evaluateExpression(value), situation)

c'est à dire une évaluation ligne par ligne. Or si certaines valeurs de
la situation contiennent des références, il faut les évaluer dans le bon
ordre.

Avec cette modification, seul le parsage est fait lorsqu'on appelle
`setSituation` et l'évaluation est faite ultérieurement lorsque c'est
nécessaire avec la même logique que pour les règles.

L'implémentation a pour effet de bord de ne plus supprimer l'utilisation
de true / false dans la situation qui doivent être remplacés par "oui"
et "non".
2020-10-11 16:51:19 +02:00

338 lines
9.5 KiB
TypeScript

import { Leaf } from './components/mecanisms/common'
import { typeWarning } from './error'
import { evaluateApplicability } from './evaluateRule'
import { evaluateNode, mergeMissing } from './evaluation'
import { convertNodeToUnit } from './nodeUnits'
import parseRule from './parseRule'
import { disambiguateRuleReference } from './ruleUtils'
import { EvaluatedNode, ParsedRule } from './types'
import { areUnitConvertible, serializeUnit } from './units'
/**
* Statically filter out replacements from `replaceBy`.
* Note: whitelist and blacklist filtering are applicable to the replacement
* itself or any parent namespace.
*/
export const getApplicableReplacedBy = (contextRuleName, replacedBy) =>
replacedBy
.sort(
(replacement1, replacement2) =>
+!!replacement2.whiteListedNames - +!!replacement1.whiteListedNames
)
.filter(
({ whiteListedNames }) =>
!whiteListedNames ||
whiteListedNames.some(name => contextRuleName.startsWith(name))
)
.filter(
({ blackListedNames }) =>
!blackListedNames ||
blackListedNames.every(name => !contextRuleName.startsWith(name))
)
.filter(({ referenceNode }) => contextRuleName !== referenceNode.dottedName)
/**
* Filter-out and apply all possible replacements at runtime.
*/
const getApplicableReplacements = (
filter,
contextRuleName,
cache,
situation,
rules,
rule: ParsedRule
) => {
let missingVariableList: Array<EvaluatedNode['missingVariables']> = []
if (contextRuleName.startsWith('[evaluation]')) {
return [[], []]
}
const applicableReplacements = getApplicableReplacedBy(
contextRuleName,
rule.replacedBy
)
// Remove remplacement defined in a not applicable node
.filter(({ referenceNode }) => {
const referenceRule = rules[referenceNode.dottedName]
const {
nodeValue: isApplicable,
missingVariables
} = evaluateApplicability(cache, situation, rules, referenceRule)
missingVariableList.push(missingVariables)
return isApplicable
})
// Remove remplacement defined in a node whose situation value is false
.filter(({ referenceNode }) => {
const referenceRule = rules[referenceNode.dottedName]
const situationValue = situation[referenceRule.dottedName]
if (referenceNode.question && situationValue == null) {
missingVariableList.push({ [referenceNode.dottedName]: 1 })
}
return situationValue?.nodeValue !== false
})
// Remove remplacement defined in a boolean node whose evaluated value is false
.filter(({ referenceNode }) => {
const referenceRule = rules[referenceNode.dottedName]
if (referenceRule.formule?.explanation?.operationType !== 'comparison') {
return true
}
const { nodeValue: isApplicable, missingVariables } = evaluateNode(
cache,
situation,
rules,
referenceRule
)
missingVariableList.push(missingVariables)
return isApplicable
})
.map(({ referenceNode, replacementNode }) =>
replacementNode != null
? evaluateNode(cache, situation, rules, replacementNode)
: evaluateReference(filter, '')(cache, situation, rules, referenceNode)
)
.map(replacementNode => {
const replacedRuleUnit = rule.unit
if (!areUnitConvertible(replacementNode.unit, replacedRuleUnit)) {
typeWarning(
contextRuleName,
`L'unité de la règle de remplacement n'est pas compatible avec celle de la règle remplacée ${rule.dottedName}`
)
}
return {
...replacementNode,
unit: replacementNode.unit || replacedRuleUnit
}
})
missingVariableList = missingVariableList.filter(
missingVariables => !!Object.keys(missingVariables).length
)
return [applicableReplacements, missingVariableList]
}
const evaluateReference = (filter: string, contextRuleName: string) => (
cache,
situation,
rules,
node
) => {
const rule = rules[node.dottedName]
// When a rule exists in different version (created using the `replace` mecanism), we add
// a redirection in the evaluation of references to use a potential active replacement
const [
applicableReplacements,
replacementMissingVariableList
] = getApplicableReplacements(
filter,
contextRuleName,
cache,
situation,
rules,
rule
)
if (applicableReplacements.length) {
if (applicableReplacements.length > 1) {
// eslint-disable-next-line no-console
console.warn(`
Règle ${rule.dottedName}: plusieurs remplacements valides ont été trouvés :
\n\t${applicableReplacements.map(node => node.rawNode).join('\n\t')}
Par défaut, seul le premier s'applique. Si vous voulez un autre comportement, vous pouvez :
- Restreindre son applicabilité via "applicable si" sur la règle de définition
- Restreindre sa portée en ajoutant une liste blanche (via le mot clé "dans") ou une liste noire (via le mot clé "sauf dans")
`)
}
return applicableReplacements[0]
}
const addReplacementMissingVariable = node => ({
...node,
missingVariables: replacementMissingVariableList.reduce(
mergeMissing,
node.missingVariables
)
})
const dottedName = node.dottedName
// On va vérifier dans le cache courant, dict, si la variable n'a pas été déjà évaluée
// En effet, l'évaluation dans le cas d'une variable qui a une formule, est coûteuse !
const cacheName = dottedName + (filter ? ' .' + filter : '')
const cached = cache[cacheName]
if (cached) return addReplacementMissingVariable(cached)
const cacheNode = (
nodeValue: EvaluatedNode['nodeValue'],
missingVariables: EvaluatedNode['missingVariables'],
explanation?: Record<string, unknown>
) => {
cache[cacheName] = {
...node,
nodeValue,
...(explanation && {
explanation
}),
...(explanation?.temporalValue && {
temporalValue: explanation.temporalValue
}),
...(explanation?.unit && { unit: explanation.unit }),
missingVariables
}
return addReplacementMissingVariable(cache[cacheName])
}
const applicabilityEvaluation = evaluateApplicability(
cache,
situation,
rules,
rule
)
if (!applicabilityEvaluation.nodeValue) {
return cacheNode(
applicabilityEvaluation.nodeValue,
applicabilityEvaluation.missingVariables,
applicabilityEvaluation
)
}
if (situation[dottedName]) {
// Conditional evaluation is required because some mecanisms like
// "synchronisation" store raw JS objects in the situation.
const situationValue = situation[dottedName]?.evaluate
? evaluateNode(cache, situation, rules, situation[dottedName])
: situation[dottedName]
const unit =
!situationValue.unit || serializeUnit(situationValue.unit) === ''
? rule.unit
: situationValue.unit
return cacheNode(
situationValue?.nodeValue !== undefined
? situationValue.nodeValue
: situationValue,
applicabilityEvaluation.missingVariables,
{
...rule,
...(situationValue?.nodeValue !== undefined && situationValue),
unit
}
)
}
if (rule.defaultValue != null) {
const evaluation = evaluateNode(cache, situation, rules, rule.defaultValue)
return cacheNode(evaluation.nodeValue ?? evaluation, {
...evaluation.missingVariables,
[dottedName]: 1
})
}
if (rule.formule != null) {
const evaluation = evaluateNode(cache, situation, rules, rule)
return cacheNode(
evaluation.nodeValue,
evaluation.missingVariables,
evaluation
)
}
return cacheNode(null, { [dottedName]: 2 })
}
export const parseReference = (
rules,
rule,
parsedRules,
filter
) => partialReference => {
const dottedName = disambiguateRuleReference(
rules,
rule.dottedName,
partialReference
)
const inInversionFormula = rule.formule?.['inversion numérique']
const parsedRule =
parsedRules[dottedName] ||
// the 'inversion numérique' formula should not exist. The instructions to the evaluation should be enough to infer that an inversion is necessary (assuming it is possible, the client decides this)
(!inInversionFormula && parseRule(rules, dottedName, parsedRules))
const unit = parsedRule.unit
return {
evaluate: evaluateReference(filter, rule.dottedName),
jsx: Leaf,
name: partialReference,
category: 'reference',
partialReference,
dottedName,
explanation: parsedRule,
unit
}
}
// This function is a wrapper that can apply :
// - unit transformations to the value of the variable.
// See the unité-temporelle.yaml test suite for details
// - filters on the variable to select one part of the variable's 'composantes'
const evaluateTransforms = (originalEval, _, parseResult) => (
cache,
situation,
parsedRules,
node
) => {
// Filter transformation
const filteringSituation = {
...situation,
'_meta.filter': parseResult.filter
}
const filteredNode = originalEval(
cache,
parseResult.filter ? filteringSituation : situation,
parsedRules,
node
)
const { explanation, nodeValue } = filteredNode
if (!explanation || nodeValue === null) {
return filteredNode
}
const unit = parseResult.unit
if (unit) {
try {
return convertNodeToUnit(unit, filteredNode)
} catch (e) {
typeWarning(
cache._meta.contextRule,
`Impossible de convertir la reference '${filteredNode.name}'`,
e
)
}
}
return filteredNode
}
export const parseReferenceTransforms = (
rules,
rule,
parsedRules
) => parseResult => {
const referenceName = parseResult.variable.fragments.join(' . ')
const node = parseReference(
rules,
rule,
parsedRules,
parseResult.filter
)(referenceName)
return {
...node,
// Decorate node with the composante filter (either who is paying, either tax free)
...(parseResult.filter
? {
cotisation: {
...(node as any).cotisation,
'dû par': parseResult.filter,
'impôt sur le revenu': parseResult.filter
}
}
: {}),
evaluate: evaluateTransforms(node.evaluate, rule, parseResult),
unit: parseResult.unit || node.unit
}
}