Linux GNU 11.4.0 Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 78.3% 166 / 0 / 212
Functions: -% 0 / 1 / 1
Branches: 67.5% 181 / 0 / 268

OMCompiler/Compiler/NFFrontEnd/NFClassDiagram.mo
Line Branch Exec Source
1 /*
2 * This file is part of OpenModelica.
3 *
4 * Copyright (c) 1998-2026, Open Source Modelica Consortium (OSMC),
5 * c/o Linköpings universitet, Department of Computer and Information Science,
6 * SE-58183 Linköping, Sweden.
7 *
8 * All rights reserved.
9 *
10 * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR
11 * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8.
12 * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
13 * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL
14 * VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
15 *
16 * The OpenModelica software and the OSMC (Open Source Modelica Consortium)
17 * Public License (OSMC-PL) are obtained from OSMC, either from the above
18 * address, from the URLs:
19 * http://www.openmodelica.org or
20 * https://github.com/OpenModelica/ or
21 * http://www.ida.liu.se/projects/OpenModelica,
22 * and in the OpenModelica distribution.
23 *
24 * GNU AGPL version 3 is obtained from:
25 * https://www.gnu.org/licenses/licenses.html#GPL
26 *
27 * This program is distributed WITHOUT ANY WARRANTY; without
28 * even the implied warranty of MERCHANTABILITY or FITNESS
29 * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH
30 * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL.
31 *
32 * See the full OSMC Public License conditions for more details.
33 *
34 */
35
36 encapsulated package NFClassDiagram
37 "UML class diagrams of Modelica classes as PlantUML, Mermaid or draw.io text, used by
38 getClassDiagram.
39
40 The diagram has the class, the classes it extends and the classes they are
41 redeclared from (class extends), and the classes it uses, up to a number of
42 levels: the types of its components and the classes that replaceable classes
43 default to, are constrained by or are redeclared as. The names are looked up
44 with the walker of NFUsedElements, which follows imports, base classes and
45 redeclares without instantiating anything; the declarations, prefixes and
46 modifiers are taken from the SCode."
47
48 import Absyn;
49 import SCode;
50
51 protected
52 import AbsynUtil;
53 import Dump;
54 import NFUsedElements;
55 import NFUsedElements.Definition;
56 import NFUsedElements.Use;
57 import Pointer;
58 import SCodeDump;
59 import SCodeUtil;
60 import System;
61 import UnorderedMap;
62 import UnorderedSet;
63
64 constant Integer GENERALIZATION = 1;
65 constant Integer COMPOSITION = 2;
66 constant Integer DEPENDENCY = 3;
67 constant Integer NESTING = 4;
68
69 constant Integer INHERITED = 0 "A base class or a nested class, same level.";
70 constant Integer USED = 1 "A used class, one level more.";
71 constant Integer DRAWN = -1 "Only drawn if the class is in the diagram anyway.";
72
73 uniontype Node
74 record NODE
75 String name "The full name of the class.";
76 String id "The name of the class in the diagram text.";
77 SCode.Element element;
78 Integer level "Levels of uses from the class the diagram is of.";
79 end NODE;
80 end Node;
81
82 uniontype Relation
83 record RELATION
84 Integer kind;
85 String target "The full name of the class it points to.";
86 String label;
87 String multiplicity;
88 Integer step "INHERITED, USED or DRAWN.";
89 String declaredBy "The short class definition it's declared by, if any.";
90 end RELATION;
91 end Relation;
92
93 uniontype Diagram
94 record DIAGRAM
95 UnorderedMap<String, Node> nodes "By full name.";
96 Pointer<list<String>> order "The full names of the nodes, in reverse order.";
97 UnorderedSet<String> ids;
98 UnorderedMap<String, String> resolved "The full name a name refers to, by scope and name.";
99 SCode.Program program;
100 Integer depth;
101 list<String> exclude;
102 Boolean showModifiers;
103 end DIAGRAM;
104 end Diagram;
105
106 public
107 function generate
108 "Returns the diagram of a class, or an empty string if the class isn't found."
109 input Absyn.Path className;
110 input SCode.Program program "Including the builtin classes.";
111 input SCode.Program annotationProgram;
112 input String format "plantuml, mermaid or drawio.";
113 input Integer depth "Levels of used classes.";
114 input list<String> exclude "Classes and packages to leave out.";
115 input Boolean showModifiers;
116 output String diagram = "";
117 protected
118 Diagram d;
119 list<String> pending, batch;
120 list<Absyn.Path> paths;
121 list<Definition> defs;
122 list<Use> uses;
123 String name;
124 algorithm
125
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
6 d := DIAGRAM(UnorderedMap.new<Node>(stringHashDjb2, stringEq), Pointer.create({}),
126 UnorderedSet.new<String>(stringHashDjb2, stringEq),
127 UnorderedMap.new<String>(stringHashDjb2, stringEq),
128 program, depth, exclude, showModifiers);
129 5 name := AbsynUtil.pathString(AbsynUtil.makeNotFullyQualified(className));
130 5 pending := addNode(name, 0, true, d, {});
131
132
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if listEmpty(pending) then
133 ✗ return;
134 end if;
135
136 // The classes are walked a level at a time, since the classes a class uses
137 // are only known once its names are looked up.
138
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 5 times.
17 while not listEmpty(pending) loop
139 12 batch := listReverse(pending);
140 pending := {};
141 paths := {};
142
143
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 12 times.
29 for n in batch loop
144 17 paths := walkedPaths(UnorderedMap.getOrFail(n, d.nodes), paths);
145 end for;
146
147 12 (defs, uses, _) := NFUsedElements.collectUses(listReverse(paths), program, annotationProgram);
148 12 addResolved(defs, uses, d.resolved);
149
150
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 12 times.
29 for n in batch loop
151 17 pending := expand(UnorderedMap.getOrFail(n, d.nodes), d, pending);
152 end for;
153 end while;
154
155
4/8
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 4 times.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
5 diagram := if format == "drawio" then drawio(name, d)
156 elseif format == "mermaid" then mermaid(d) else plantuml(d);
157 end generate;
158
159 protected
160 function addNode
161 "Adds a class to the diagram, and to the classes to walk if it's new or
162 found on a lower level."
163 input String name;
164 input Integer level;
165 input Boolean isRoot;
166 input Diagram d;
167 input output list<String> pending;
168 protected
169 SCode.Element cls;
170 Node node;
171 algorithm
172
5/6
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 12 times.
✓ Branch 3 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
19 if not isRoot and (level > d.depth or isExcluded(name, d.exclude)) then
173 2 return;
174 end if;
175
176 () := match UnorderedMap.get(name, d.nodes)
177 case SOME(node)
178 algorithm
179 ✗ if level < node.level then
180 ✗ node.level := level;
181 ✗ UnorderedMap.add(name, node, d.nodes);
182
183 ✗ if not listMember(name, pending) then
184 pending := name :: pending;
185 end if;
186 end if;
187 then
188 ();
189
190 else
191 algorithm
192 () := match findClass(name, d.program)
193 case SOME(cls) guard isRoot or isDiagramClass(cls)
194 algorithm
195 17 UnorderedMap.add(name, NODE(name, newId(name, d.ids), cls, level), d.nodes);
196 34 Pointer.update(d.order, name :: Pointer.access(d.order));
197 pending := name :: pending;
198 then
199 ();
200
201 else ();
202 end match;
203 then
204 ();
205 end match;
206 end addNode;
207
208 function isExcluded
209 input String name;
210 input list<String> exclude;
211 output Boolean res = false;
212 algorithm
213
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 12 times.
24 for e in exclude loop
214
5/10
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✓ Branch 7 taken 8 times.
✓ Branch 9 taken 4 times.
✗ Branch 10 not taken.
✗ Branch 13 not taken.
✓ Branch 14 taken 4 times.
12 if name == e or (stringLength(name) > stringLength(e) and
215 substring(name, 1, stringLength(e) + 1) == e + ".") then
216 res := true;
217 ✗ return;
218 end if;
219 end for;
220 end isExcluded;
221
222 function isDiagramClass
223 "Types and enumerations are only shown as the types of components."
224 input SCode.Element cls;
225 output Boolean res;
226 algorithm
227 res := match cls
228 case SCode.CLASS(classDef = SCode.ENUMERATION()) then false;
229 case SCode.CLASS(restriction = SCode.R_TYPE()) then false;
230 case SCode.CLASS(restriction = SCode.R_ENUMERATION()) then false;
231 case SCode.CLASS(restriction = SCode.R_PREDEFINED_INTEGER()) then false;
232 case SCode.CLASS(restriction = SCode.R_PREDEFINED_REAL()) then false;
233 case SCode.CLASS(restriction = SCode.R_PREDEFINED_STRING()) then false;
234 case SCode.CLASS(restriction = SCode.R_PREDEFINED_BOOLEAN()) then false;
235 case SCode.CLASS(restriction = SCode.R_PREDEFINED_CLOCK()) then false;
236 case SCode.CLASS(restriction = SCode.R_PREDEFINED_ENUMERATION()) then false;
237 case SCode.CLASS() then true;
238 else false;
239 end match;
240 end isDiagramClass;
241
242 function newId
243 "Returns a unique name for a class in the diagram text, its full name with
244 the characters that aren't letters, digits or underscores replaced."
245 input String name;
246 input UnorderedSet<String> ids;
247 output String id;
248 protected
249 String base;
250 Integer i = 1;
251 algorithm
252
6/6
✓ Branch 1 taken 146 times.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 146 times.
✓ Branch 4 taken 17 times.
✓ Branch 6 taken 21 times.
✓ Branch 7 taken 125 times.
163 base := stringAppendList(list(if isIdChar(c) then c else "_" for c in stringListStringChar(name)));
253 id := base;
254
255
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
17 while UnorderedSet.contains(id, ids) loop
256 ✗ i := i + 1;
257 ✗ id := base + "_" + intString(i);
258 end while;
259
260 17 UnorderedSet.add(id, ids);
261 end newId;
262
263 function isIdChar
264 input String c;
265 output Boolean res;
266 protected
267 Integer i = stringCharInt(c);
268 algorithm
269
5/6
✓ Branch 0 taken 108 times.
✓ Branch 1 taken 38 times.
✓ Branch 2 taken 21 times.
✓ Branch 3 taken 87 times.
✓ Branch 4 taken 21 times.
✗ Branch 5 not taken.
146 res := (i >= 48 and i <= 57) or (i >= 65 and i <= 90) or (i >= 97 and i <= 122) or i == 95;
270 end isIdChar;
271
272 function findClass
273 "Returns the class declared with the given full name."
274 input String name;
275 input SCode.Program program;
276 output Option<SCode.Element> cls = NONE();
277 protected
278 list<SCode.Element> elements = program;
279 Absyn.Path path;
280 String id;
281 Boolean found;
282 algorithm
283 17 path := AbsynUtil.stringPath(name);
284
285 21 while true loop
286 38 id := AbsynUtil.pathFirstIdent(path);
287 found := false;
288
289
1/2
✓ Branch 0 taken 1725 times.
✗ Branch 1 not taken.
1725 for e in elements loop
290
6/6
✓ Branch 1 taken 1721 times.
✓ Branch 2 taken 4 times.
✓ Branch 4 taken 40 times.
✓ Branch 5 taken 1681 times.
✓ Branch 8 taken 38 times.
✓ Branch 9 taken 2 times.
1725 if SCodeUtil.elementIsClass(e) and SCodeUtil.elementName(e) == id then
291 cls := SOME(e);
292 38 elements := SCodeUtil.getClassElements(e);
293 found := true;
294 38 break;
295 end if;
296 end for;
297
298
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 38 times.
38 if not found then
299 cls := NONE();
300 ✗ return;
301 elseif not AbsynUtil.pathIsQual(path) then
302 17 return;
303 end if;
304
305 21 path := AbsynUtil.pathRest(path);
306 end while;
307 end findClass;
308
309 function walkedPaths
310 "Adds the paths to walk to find the names a node uses: the class itself and
311 the short class definitions in it, which are their own scopes."
312 input Node node;
313 input output list<Absyn.Path> paths;
314 protected
315 Absyn.Path path = AbsynUtil.stringPath(node.name);
316 algorithm
317 paths := path :: paths;
318
319
2/2
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 17 times.
46 for e in SCodeUtil.getClassElements(node.element) loop
320
4/4
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 23 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 4 times.
29 if SCodeUtil.elementIsClass(e) and SCodeUtil.isDerivedClass(e) then
321 2 paths := AbsynUtil.suffixPath(path, SCodeUtil.elementName(e)) :: paths;
322 end if;
323 end for;
324 end walkedPaths;
325
326 function addResolved
327 "Records the full name each name used in the walked classes refers to, by
328 the class it's used in and the name as written."
329 input list<Definition> defs;
330 input list<Use> uses;
331 input UnorderedMap<String, String> resolved;
332 protected
333 UnorderedMap<String, String> names = UnorderedMap.new<String>(stringHashDjb2, stringEq);
334 String role;
335 algorithm
336
2/2
✓ Branch 0 taken 63 times.
✓ Branch 1 taken 12 times.
75 for d in defs loop
337 63 UnorderedMap.add(d.key, d.name, names);
338 end for;
339
340
2/2
✓ Branch 0 taken 41 times.
✓ Branch 1 taken 12 times.
53 for u in uses loop
341 41 role := u.site.role;
342
343 // Modifier names are looked up in the class of what's modified, and the
344 // name after end is the class itself. Only the whole name is kept.
345
7/12
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 34 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 34 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✓ Branch 13 taken 28 times.
✓ Branch 14 taken 6 times.
41 if not u.candidate and role <> "modifier" and role <> "end" and
346 u.index == pathLength(u.written) then
347
6/8
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 26 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 6 times.
✓ Branch 7 taken 20 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 6 times.
28 UnorderedMap.add(resolvedKey(u.site.scope, AbsynUtil.pathString(
348 AbsynUtil.makeNotFullyQualified(u.written)), role == "classExtends" or role == "redeclare"),
349 UnorderedMap.getOrFail(u.key, names), resolved);
350 end if;
351 end for;
352 end addResolved;
353
354 function pathLength
355 input Absyn.Path path;
356 output Integer len;
357 algorithm
358 len := match path
359 12 case Absyn.Path.QUALIFIED() then 1 + pathLength(path.path);
360 ✗ case Absyn.Path.FULLYQUALIFIED() then pathLength(path.path);
361 else 1;
362 end match;
363 end pathLength;
364
365 function resolvedKey
366 input String scope;
367 input String name;
368 input Boolean classExtends "The name of a class extends or redeclared class, the class it replaces.";
369 output String key = stringAppendList({scope, if classExtends then " extends " else " ", name});
370 end resolvedKey;
371
372 function resolve
373 "Returns the full name a name used in a class refers to, or an empty string."
374 input String scope;
375 input Absyn.Path name;
376 input Diagram d;
377 input Boolean classExtends = false;
378 output String fullName;
379 algorithm
380 56 fullName := UnorderedMap.getOrDefault(resolvedKey(scope,
381 AbsynUtil.pathString(AbsynUtil.makeNotFullyQualified(name)), classExtends), d.resolved, "");
382 end resolve;
383
384 function expand
385 "Adds the classes a node points to."
386 input Node node;
387 input Diagram d;
388 input output list<String> pending;
389 algorithm
390
2/2
✓ Branch 1 taken 34 times.
✓ Branch 2 taken 17 times.
51 for r in relations(node, d) loop
391
4/4
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 16 times.
34 if r.step <> DRAWN and not stringEmpty(r.target) then
392 14 pending := addNode(r.target, node.level + r.step, false, d, pending);
393 end if;
394 end for;
395 end expand;
396
397 function relations
398 "Returns what a class points to, in the order it's declared."
399 input Node node;
400 input Diagram d;
401 output list<Relation> rels = {};
402 protected
403 String scope = node.name, target, name;
404 SCode.Element cls = node.element;
405 Absyn.TypeSpec ts;
406 SCode.Mod mod;
407 algorithm
408 rels := match cls
409 case SCode.CLASS(classDef = SCode.DERIVED(typeSpec = ts, modifications = mod))
410 algorithm
411 ✗ target := resolve(scope, AbsynUtil.typeSpecPath(ts), d);
412 ✗ rels := RELATION(GENERALIZATION, target, modString(mod, d), "", INHERITED, "") :: rels;
413 ✗ then
414 redeclares(mod, "", scope, d, rels);
415
416 case SCode.CLASS(name = name, classDef = SCode.CLASS_EXTENDS(modifications = mod))
417 algorithm
418 4 target := resolve(scope, Absyn.IDENT(name), d, classExtends = true);
419 4 rels := RELATION(GENERALIZATION, target, modString(mod, d), "", INHERITED, "") :: rels;
420 4 then
421 redeclares(mod, "", scope, d, rels);
422
423 else rels;
424 end match;
425
426 // A redeclared class that doesn't extend the class it replaces.
427
3/4
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 30 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4 times.
34 if SCodeUtil.isElementRedeclare(cls) and not SCodeUtil.isClassExtends(cls) then
428 ✗ target := resolve(scope, Absyn.IDENT(SCodeUtil.elementName(cls)), d, classExtends = true);
429 ✗ rels := RELATION(DEPENDENCY, target, "redeclares", "", INHERITED, "") :: rels;
430 end if;
431
432
2/2
✓ Branch 1 taken 58 times.
✓ Branch 2 taken 34 times.
92 for e in SCodeUtil.getClassElements(cls) loop
433 58 rels := elementRelations(e, scope, d, rels);
434 end for;
435
436 34 rels := listReverse(rels);
437 end relations;
438
439 function elementRelations
440 input SCode.Element element;
441 input String scope;
442 input Diagram d;
443 input output list<Relation> rels;
444 protected
445 String target, name, full_name;
446 Absyn.Path path;
447 Absyn.TypeSpec ts;
448 Absyn.ArrayDim dims;
449 SCode.Mod mod;
450 SCode.Prefixes prefixes;
451 algorithm
452 () := match element
453 case SCode.EXTENDS(baseClassPath = path, modifications = mod)
454 algorithm
455 8 target := resolve(scope, path, d);
456 8 rels := RELATION(GENERALIZATION, target, modString(mod, d), "", INHERITED, "") :: rels;
457 8 rels := redeclares(mod, "extends " + AbsynUtil.pathString(path), scope, d, rels);
458 then
459 ();
460
461 case SCode.COMPONENT(name = name, prefixes = prefixes, typeSpec = ts, modifications = mod,
462 attributes = SCode.ATTR(arrayDims = dims))
463 algorithm
464 38 target := resolve(scope, AbsynUtil.typeSpecPath(ts), d);
465 38 rels := RELATION(COMPOSITION, target, name, Dump.printArraydimStr(dims), USED, "") :: rels;
466 38 rels := constrainedBy(prefixes, name, {scope}, d, rels);
467 38 rels := redeclares(mod, name, scope, d, rels);
468 then
469 ();
470
471 case SCode.CLASS(name = name, prefixes = prefixes,
472 classDef = SCode.DERIVED(typeSpec = ts, modifications = mod))
473 algorithm
474 4 full_name := scope + "." + name;
475 4 path := AbsynUtil.typeSpecPath(ts);
476 4 target := resolve(full_name, path, d);
477
478
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if stringEmpty(target) then
479 ✗ target := resolve(scope, path, d);
480 end if;
481
482 4 rels := RELATION(NESTING, full_name, "", "", DRAWN, "") :: rels;
483 4 rels := RELATION(DEPENDENCY, target, stereotype(element) + " " + name, "", USED, full_name) :: rels;
484 4 rels := constrainedBy(prefixes, name, {full_name, scope}, d, rels);
485 4 rels := redeclares(mod, name, full_name, d, rels);
486 then
487 ();
488
489 // Class extends and redeclared classes are part of the class, other
490 // classes declared in it are only drawn if they're in the diagram anyway.
491 case SCode.CLASS(name = name)
492 algorithm
493
3/4
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 4 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
8 rels := RELATION(NESTING, scope + "." + name, "", "",
494 if SCodeUtil.isClassExtends(element) or SCodeUtil.isElementRedeclare(element)
495 then INHERITED else DRAWN, "") :: rels;
496 then
497 ();
498
499 else ();
500 end match;
501 end elementRelations;
502
503 function constrainedBy
504 input SCode.Prefixes prefixes;
505 input String name;
506 input list<String> scopes "The classes to look the name up in, in order.";
507 input Diagram d;
508 input output list<Relation> rels;
509 protected
510 String target = "";
511 Absyn.Path path;
512 algorithm
513 () := match prefixes
514 case SCode.PREFIXES(replaceablePrefix = SCode.REPLACEABLE(cc = SOME(SCode.CONSTRAINCLASS(constrainingClass = path))))
515 algorithm
516 ✗ for s in scopes loop
517 ✗ target := resolve(s, path, d);
518
519 ✗ if not stringEmpty(target) then
520 break;
521 end if;
522 end for;
523
524 ✗ rels := RELATION(DEPENDENCY, target, name + " constrainedby", "", USED, "") :: rels;
525 then
526 ();
527
528 else ();
529 end match;
530 end constrainedBy;
531
532 function redeclares
533 "Adds the classes redeclared in a modifier, as dependencies of the class the
534 modifier is in."
535 input SCode.Mod mod;
536 input String owner "What the modifier modifies, e.g. the component name.";
537 input String scope;
538 input Diagram d;
539 input output list<Relation> rels;
540 protected
541 String target, label;
542 list<SCode.SubMod> submods;
543 SCode.Element e;
544 Absyn.TypeSpec ts;
545 algorithm
546 () := match mod
547 case SCode.MOD(subModLst = submods)
548 algorithm
549
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 46 times.
60 for sm in submods loop
550 14 rels := redeclares(sm.mod, owner, scope, d, rels);
551 end for;
552 then
553 ();
554
555 case SCode.REDECL(element = e)
556 algorithm
557 target := match e
558 2 case SCode.CLASS(classDef = SCode.DERIVED(typeSpec = ts)) then resolve(scope, AbsynUtil.typeSpecPath(ts), d);
559 ✗ case SCode.COMPONENT(typeSpec = ts) then resolve(scope, AbsynUtil.typeSpecPath(ts), d);
560 else "";
561 end match;
562
563 2 label := elementString(e, d);
564
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 label := if stringEmpty(owner) then label else owner + "(" + label + ")";
565 2 rels := RELATION(DEPENDENCY, target, label, "", USED, "") :: rels;
566 then
567 ();
568
569 else ();
570 end match;
571 end redeclares;
572
573 function modString
574 input SCode.Mod mod;
575 input Diagram d;
576 output String str;
577 algorithm
578
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 4 times.
12 str := if d.showModifiers then oneLine(SCodeDump.printModStr(mod, SCodeDump.generateOptions(stripStringComments = true))) else "";
579 end modString;
580
581 function elementString
582 "Returns an element as it would be declared, without comments, annotations
583 and, if the modifiers aren't shown, modifiers."
584 input SCode.Element element;
585 input Diagram d;
586 output String str;
587 protected
588 SCode.Element e = element;
589 Absyn.TypeSpec ts;
590 SCode.Attributes attr;
591 algorithm
592 () := match e
593 case SCode.COMPONENT()
594 algorithm
595
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 16 times.
19 e.comment := SCode.noComment;
596
597
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 16 times.
19 if not d.showModifiers then
598 3 e.modifications := SCode.NOMOD();
599 end if;
600 then
601 ();
602
603 case SCode.CLASS(classDef = SCode.DERIVED(typeSpec = ts, attributes = attr))
604 algorithm
605
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 e.cmt := SCode.noComment;
606
607
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if not d.showModifiers then
608 ✗ e.classDef := SCode.DERIVED(ts, SCode.NOMOD(), attr);
609 end if;
610 then
611 ();
612
613 else ();
614 end match;
615
616 23 str := oneLine(SCodeDump.unparseElementStr(e, SCodeDump.generateOptions(stripStringComments = true)));
617
618
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 23 times.
✓ Branch 7 taken 23 times.
✗ Branch 8 not taken.
23 if stringLength(str) > 0 and substring(str, stringLength(str), stringLength(str)) == ";" then
619 ✗ str := substring(str, 1, stringLength(str) - 1);
620 end if;
621 end elementString;
622
623 function oneLine
624 input String str;
625 output String res;
626 algorithm
627 31 res := System.trim(System.stringReplace(System.stringReplace(str, "\r", ""), "\n", " "));
628
629
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
31 while System.stringFind(res, " ") >= 0 loop
630 ✗ res := System.stringReplace(res, " ", " ");
631 end while;
632 end oneLine;
633
634 function stereotype
635 "Returns how a class is declared, e.g. replaceable function or redeclare
636 model extends."
637 input SCode.Element cls;
638 output String str;
639 protected
640 list<String> strl = {};
641 SCode.Restriction restriction;
642 algorithm
643
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 22 times.
24 if SCodeUtil.isElementRedeclare(cls) then
644 strl := "redeclare" :: strl;
645 end if;
646
647
2/2
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 18 times.
24 if SCodeUtil.isElementReplaceable(cls) then
648 strl := "replaceable" :: strl;
649 end if;
650
651 () := match cls
652 case SCode.CLASS(restriction = restriction)
653 algorithm
654 24 strl := SCodeDump.restrString(restriction) :: strl;
655
656
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 22 times.
24 if SCodeUtil.isClassExtends(cls) then
657 strl := "extends" :: strl;
658 end if;
659 then
660 ();
661
662 else ();
663 end match;
664
665 24 str := stringDelimitList(listReverse(strl), " ");
666 end stereotype;
667
668 function members
669 "Returns the lines shown in the box of a class: its components, short class
670 definitions and the replaceable classes that aren't in the diagram, and the
671 elements they show."
672 input Node node;
673 input Diagram d;
674 output list<String> lines = {};
675 output list<SCode.Element> elements = {};
676 protected
677 String vis, name;
678 algorithm
679
2/2
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 17 times.
46 for e in SCodeUtil.getClassElements(node.element) loop
680
1/2
✓ Branch 1 taken 29 times.
✗ Branch 2 not taken.
29 vis := if SCodeUtil.isElementProtected(e) then "- " else "+ ";
681
682 () := match e
683 case SCode.COMPONENT()
684 algorithm
685 19 lines := (vis + elementString(e, d)) :: lines;
686 elements := e :: elements;
687 then
688 ();
689
690 case SCode.CLASS(classDef = SCode.DERIVED())
691 algorithm
692 2 lines := (vis + elementString(e, d)) :: lines;
693 elements := e :: elements;
694 then
695 ();
696
697 case SCode.CLASS(name = name)
698 guard SCodeUtil.isElementReplaceable(e) and
699 not UnorderedMap.contains(node.name + "." + name, d.nodes)
700 algorithm
701 ✗ lines := (vis + stereotype(e) + " " + name) :: lines;
702 elements := e :: elements;
703 then
704 ();
705
706 else ();
707 end match;
708 end for;
709
710 17 lines := listReverse(lines);
711 17 elements := listReverse(elements);
712 end members;
713
714 function nodes
715 input Diagram d;
716 output list<Node> res;
717 algorithm
718
4/4
✓ Branch 2 taken 34 times.
✓ Branch 3 taken 10 times.
✓ Branch 4 taken 34 times.
✓ Branch 5 taken 10 times.
44 res := list(UnorderedMap.getOrFail(n, d.nodes) for n in listReverse(Pointer.access(d.order)));
719 end nodes;
720
721 function edges
722 "Returns the relations between the nodes, as source, target and relation."
723 input Diagram d;
724 output list<tuple<Node, Node, Relation>> res = {};
725 protected
726 UnorderedSet<String> seen = UnorderedSet.new<String>(stringHashDjb2, stringEq);
727 String key;
728 Node target;
729 algorithm
730
2/2
✓ Branch 1 taken 17 times.
✓ Branch 2 taken 5 times.
22 for n in nodes(d) loop
731
2/2
✓ Branch 1 taken 34 times.
✓ Branch 2 taken 17 times.
51 for r in relations(n, d) loop
732 // A short class definition in the diagram shows what it's defined as itself.
733
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
34 if not stringEmpty(r.declaredBy) and UnorderedMap.contains(r.declaredBy, d.nodes) then
734 ✗ continue;
735 end if;
736
737 () := match UnorderedMap.get(r.target, d.nodes)
738 case SOME(target)
739 algorithm
740 30 key := stringDelimitList({intString(r.kind), n.id, target.id, r.label}, " ");
741
742
1/2
✓ Branch 1 taken 15 times.
✗ Branch 2 not taken.
15 if not UnorderedSet.contains(key, seen) then
743 15 UnorderedSet.add(key, seen);
744 15 res := (n, target, r) :: res;
745 end if;
746 then
747 ();
748
749 else ();
750 end match;
751 end for;
752 end for;
753
754 5 res := listReverse(res);
755 end edges;
756
757 function plantuml
758 input Diagram d;
759 output String str;
760 protected
761 list<String> strl = {"hide empty members", "skinparam classAttributeIconSize 0", "@startuml"};
762 list<String> lines;
763 Node source, target;
764 Relation r;
765 algorithm
766
2/2
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 4 times.
18 for n in nodes(d) loop
767 14 lines := members(n, d);
768
4/4
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 4 times.
✓ Branch 6 taken 11 times.
✓ Branch 7 taken 3 times.
70 strl := stringAppendList({if SCodeUtil.isPartial(n.element) then "abstract class \"" else "class \"",
769 n.name, "\" as ", n.id, " <<", stereotype(n.element), ">>", if listEmpty(lines) then "" else " {"}) :: strl;
770
771
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 4 times.
14 if not listEmpty(lines) then
772
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 10 times.
26 for l in lines loop
773 16 strl := (" {field} " + l) :: strl;
774 end for;
775
776 strl := "}" :: strl;
777 end if;
778 end for;
779
780
2/2
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 4 times.
17 for e in edges(d) loop
781 13 (source, target, r) := e;
782
783
10/10
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 8 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 6 times.
✓ Branch 6 taken 1 time.
✓ Branch 7 taken 1 time.
✓ Branch 12 taken 2 times.
✓ Branch 13 taken 4 times.
✓ Branch 18 taken 6 times.
✓ Branch 19 taken 7 times.
26 strl := (stringAppendList(
784 if r.kind == GENERALIZATION then {target.id, " <|-- ", source.id}
785 elseif r.kind == COMPOSITION then {source.id, " *-- ",
786 if stringEmpty(r.multiplicity) then "" else "\"" + r.multiplicity + "\" ", target.id}
787 elseif r.kind == DEPENDENCY then {source.id, " ..> ", target.id}
788 else {source.id, " +-- ", target.id}) + (if stringEmpty(r.label) then "" else " : " + r.label)) :: strl;
789 end for;
790
791 strl := "@enduml\n" :: strl;
792 4 str := stringDelimitList(listReverse(strl), "\n");
793 end plantuml;
794
795 function mermaid
796 "Returns the diagram as a Mermaid class diagram. Mermaid has no nested
797 classes, so a nested class is linked to the class it's declared in."
798 input Diagram d;
799 output String str;
800 protected
801 list<String> strl = {"classDiagram"};
802 list<String> lines;
803 Node source, target;
804 Relation r;
805 String label;
806 algorithm
807 ✗ for n in nodes(d) loop
808 ✗ strl := stringAppendList({" class ", n.id, "[\"", n.name, "\"] {"}) :: strl;
809 ✗ strl := stringAppendList({" <<", if SCodeUtil.isPartial(n.element) then "partial " else "",
810 stereotype(n.element), ">>"}) :: strl;
811
812 ✗ lines := members(n, d);
813
814 ✗ for l in lines loop
815 ✗ strl := (" " + mermaidEscape(l)) :: strl;
816 end for;
817
818 strl := " }" :: strl;
819 end for;
820
821 ✗ for e in edges(d) loop
822 ✗ (source, target, r) := e;
823 ✗ label := if r.kind == NESTING and stringEmpty(r.label) then "nested" else r.label;
824
825 ✗ strl := (stringAppendList(
826 if r.kind == GENERALIZATION then {" ", target.id, " <|-- ", source.id}
827 elseif r.kind == COMPOSITION then {" ", source.id, " *-- ",
828 if stringEmpty(r.multiplicity) then "" else "\"" + mermaidEscape(r.multiplicity) + "\" ", target.id}
829 elseif r.kind == DEPENDENCY then {" ", source.id, " ..> ", target.id}
830 else {" ", source.id, " -- ", target.id}) + (if stringEmpty(label) then "" else " : " + mermaidEscape(label))) :: strl;
831 end for;
832
833 ✗ str := stringDelimitList(listReverse("" :: strl), "\n");
834 end mermaid;
835
836 function mermaidEscape
837 "Escapes what Mermaid would read as syntax: a member ending with ) would be a
838 method, ~ a generic type, braces end the class and : starts a label."
839 input String str;
840 output String res;
841 algorithm
842 ✗ res := System.stringReplace(str, "#", "#35;");
843 ✗ res := System.stringReplace(res, "(", "#40;");
844 ✗ res := System.stringReplace(res, ")", "#41;");
845 ✗ res := System.stringReplace(res, "{", "#123;");
846 ✗ res := System.stringReplace(res, "}", "#125;");
847 ✗ res := System.stringReplace(res, "<", "#60;");
848 ✗ res := System.stringReplace(res, ">", "#62;");
849 ✗ res := System.stringReplace(res, "~", "#126;");
850 ✗ res := System.stringReplace(res, ":", "#58;");
851 end mermaidEscape;
852
853 function drawio
854 "Returns the diagram as a draw.io (diagrams.net) file. The classes are laid
855 out in rows, base classes above the classes that extend them; draw.io can
856 arrange them in other ways (Arrange > Layout). Every class links to
857 modelica://<class> and every line in it to the line of the element it shows,
858 modelica://<class>?lineNumber=<line in the file>, with &element=<name> for a
859 component."
860 input String name;
861 input Diagram d;
862 output String str;
863 protected
864 list<Node> ns = nodes(d);
865 list<tuple<Node, Node, Relation>> es = edges(d);
866 UnorderedMap<String, Integer> ranks = UnorderedMap.new<Integer>(stringHashDjb2, stringEq);
867 Integer rank, max_rank = 0, x, y = 20, w, h, row_h, i = 0;
868 list<String> strl, lines, links;
869 list<SCode.Element> elements;
870 Node source, target;
871 Relation r;
872 String label;
873 Boolean changed = true;
874 algorithm
875 // The rank of a class is one more than the rank of its lowest base class.
876
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4 for n in ns loop
877 3 UnorderedMap.add(n.name, 0, ranks);
878 end for;
879
880
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
3 while changed and i < listLength(ns) loop
881 changed := false;
882 2 i := i + 1;
883
884
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
6 for e in es loop
885 4 (source, target, r) := e;
886
887
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if r.kind == GENERALIZATION then
888 2 rank := UnorderedMap.getOrFail(target.name, ranks) + 1;
889
890
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 if rank > UnorderedMap.getOrFail(source.name, ranks) then
891 1 UnorderedMap.add(source.name, rank, ranks);
892 max_rank := max(max_rank, rank);
893 changed := true;
894 end if;
895 end if;
896 end for;
897 end while;
898
899 1 strl := {" <mxCell id=\"1\" parent=\"0\"/>", " <mxCell id=\"0\"/>",
900 " <root>", " <mxGraphModel>",
901 " <diagram id=\"" + xmlEscape(name) + "\" name=\"" + xmlEscape(name) + "\">",
902 "<mxfile host=\"OpenModelica\">"};
903
904
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 for rk in 0:max_rank loop
905 x := 20;
906 row_h := 0;
907
908
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 for n in ns loop
909
2/2
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
6 if UnorderedMap.getOrFail(n.name, ranks) == rk then
910 3 (lines, elements) := members(n, d);
911
4/4
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 3 times.
17 w := 20 + 7 * max(stringLength(l) for l in stereotype(n.element) :: n.name :: lines);
912
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 h := 44 + (if listEmpty(lines) then 0 else 8 + 16 * listLength(lines));
913
2/2
✓ Branch 5 taken 1 time.
✓ Branch 6 taken 2 times.
3 label := "<p style=\"margin:4px;text-align:center\"><i>&#171;" + htmlEscape(stereotype(n.element)) +
914 "&#187;</i><br/>" + (if SCodeUtil.isPartial(n.element) then "<b><i>" + htmlEscape(n.name) + "</i></b>"
915 else "<b>" + htmlEscape(n.name) + "</b>") + "</p>";
916
917
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if not listEmpty(lines) then
918
7/8
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
8 links := list(memberLink(n, l, e) threaded for l in lines, e in elements);
919 3 label := label + "<hr size=\"1\"/><p style=\"margin:0 4px\">" +
920 stringDelimitList(links, "<br/>") + "</p>";
921 end if;
922
923 24 strl := stringAppendList({" <UserObject id=\"", n.id, "\" label=\"", xmlEscape(label),
924 "\" link=\"", xmlEscape(classLink(n.name)), "\">",
925 "<mxCell style=\"verticalAlign=top;align=left;overflow=fill;html=1;whiteSpace=nowrap;\" vertex=\"1\" parent=\"1\">",
926 "<mxGeometry x=\"", intString(x), "\" y=\"", intString(y), "\" width=\"", intString(w),
927 "\" height=\"", intString(h), "\" as=\"geometry\"/></mxCell></UserObject>"}) :: strl;
928 3 x := x + w + 40;
929 row_h := max(row_h, h);
930 end if;
931 end for;
932
933 2 y := y + row_h + 80;
934 end for;
935
936 i := 0;
937
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
3 for e in es loop
938 2 (source, target, r) := e;
939 2 i := i + 1;
940
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
2 label := if stringEmpty(r.multiplicity) then r.label else r.label + " " + r.multiplicity;
941
942 12 strl := stringAppendList({" <mxCell id=\"e", intString(i), "\" value=\"", xmlEscape(htmlEscape(label)),
943 "\" style=\"", edgeStyle(r.kind), "html=1;\" edge=\"1\" parent=\"1\" source=\"", source.id,
944 "\" target=\"", target.id, "\"><mxGeometry relative=\"1\" as=\"geometry\"/></mxCell>"}) :: strl;
945 end for;
946
947 strl := "</mxfile>\n" :: " </diagram>" :: " </mxGraphModel>" :: " </root>" :: strl;
948 1 str := stringDelimitList(listReverse(strl), "\n");
949 end drawio;
950
951 function classLink
952 "modelica://<class>, the link Modelica documentation uses for a class."
953 input String name;
954 output String link = "modelica://" + urlEscape(name);
955 end classLink;
956
957 function memberLink
958 "Returns a line in the box of a class as a link to the line in the file the
959 element it shows is declared on, if it's known, and for a component also
960 its name, so that a viewer can select it in a graphical view instead."
961 input Node node;
962 input String line;
963 input SCode.Element element;
964 output String link;
965 protected
966 SourceInfo info = SCodeUtil.elementInfo(element);
967 String href;
968 algorithm
969 5 link := htmlEscape(line);
970
971
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if info.lineNumberStart > 0 then
972 5 href := classLink(node.name) + "?lineNumber=" + intString(info.lineNumberStart);
973
974
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 if SCodeUtil.isComponent(element) then
975 5 href := href + "&element=" + urlEscape(SCodeUtil.elementName(element));
976 end if;
977
978 5 link := "<a href=\"" + htmlEscape(href) + "\" style=\"color:inherit;text-decoration:none\">" + link + "</a>";
979 end if;
980 end memberLink;
981
982 function urlEscape
983 "Escapes what can't be in a URL path; only quoted names have these."
984 input String str;
985 output String res;
986 algorithm
987 13 res := System.stringReplace(str, "%", "%25");
988 13 res := System.stringReplace(res, " ", "%20");
989 13 res := System.stringReplace(res, "\"", "%22");
990 13 res := System.stringReplace(res, "#", "%23");
991 13 res := System.stringReplace(res, "'", "%27");
992 13 res := System.stringReplace(res, "<", "%3C");
993 13 res := System.stringReplace(res, ">", "%3E");
994 13 res := System.stringReplace(res, "?", "%3F");
995 end urlEscape;
996
997 function edgeStyle
998 input Integer kind;
999 output String style;
1000 algorithm
1001
3/18
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
2 style := if kind == GENERALIZATION then "endArrow=block;endFill=0;endSize=12;"
1002 elseif kind == COMPOSITION then "startArrow=diamondThin;startFill=1;startSize=14;endArrow=none;"
1003 elseif kind == DEPENDENCY then "dashed=1;endArrow=open;endSize=12;"
1004 else "startArrow=circlePlus;startFill=0;endArrow=none;";
1005 end edgeStyle;
1006
1007 function htmlEscape
1008 input String str;
1009 output String res;
1010 algorithm
1011 28 res := System.stringReplace(str, "&", "&amp;");
1012 28 res := System.stringReplace(res, "<", "&lt;");
1013 28 res := System.stringReplace(res, ">", "&gt;");
1014 end htmlEscape;
1015
1016 function xmlEscape
1017 input String str;
1018 output String res;
1019 algorithm
1020 10 res := htmlEscape(str);
1021 10 res := System.stringReplace(res, "\"", "&quot;");
1022 end xmlEscape;
1023
1024 annotation(__OpenModelica_Interface="nf_frontend");
1025 end NFClassDiagram;
1026