kibi-core 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/schema/entities.pl +12 -0
- package/src/cases.pl +311 -0
- package/src/checks.pl +179 -0
- package/src/discovery.pl +9 -2
- package/src/model_check.pl +209 -6
- package/src/requirement_proof.pl +467 -19
- package/src/rule_registry.pl +10 -0
package/src/model_check.pl
CHANGED
|
@@ -117,8 +117,9 @@ certificate_model(state_machine, Certificate, model(any(States), Table, machine(
|
|
|
117
117
|
; Initial = none
|
|
118
118
|
),
|
|
119
119
|
machine_table(States, Edges, Initial, Table).
|
|
120
|
-
certificate_model(source, Certificate, model(none, none, source(Members, Entities, Routes, Sequence))) :-
|
|
120
|
+
certificate_model(source, Certificate, model(none, none, source(Members, Entities, Routes, Sequence, Matching))) :-
|
|
121
121
|
!,
|
|
122
|
+
source_matching(Certificate, Matching),
|
|
122
123
|
required(Certificate, model, Model),
|
|
123
124
|
required(Model, members, RawMembers),
|
|
124
125
|
maplist(text_string, RawMembers, Members0),
|
|
@@ -306,6 +307,204 @@ relation_problem("excludes", Actual, Expected, What, Reason) :-
|
|
|
306
307
|
atomic_list_concat(Shared, ', ', Text),
|
|
307
308
|
format(string(Reason), "~w include ~w, which the fact excludes", [What, Text]).
|
|
308
309
|
|
|
310
|
+
% ---------------------------------------------------------------------------
|
|
311
|
+
% Reference members
|
|
312
|
+
%
|
|
313
|
+
% The references reader's members are dotted, slashed or ::-joined names
|
|
314
|
+
% (java.sql.Connection, github.com/labstack/echo/v4/middleware,
|
|
315
|
+
% std::env::var), a wildcard import (java.sql.*), or *.m for a call of m on a
|
|
316
|
+
% receiver the reader cannot name. For these members an expected item E
|
|
317
|
+
% covers more than its own spelling, and each operator reads it in the
|
|
318
|
+
% direction that can never prove a claim the code does not hold:
|
|
319
|
+
%
|
|
320
|
+
% excludes E fails on any member that is or may be E: E itself, a member
|
|
321
|
+
% under E at a segment boundary (java.sql covers
|
|
322
|
+
% java.sql.Connection), a member under the prefix of E* (java.sql.*
|
|
323
|
+
% covers java.sql.Connection), a wildcard import that may bring E
|
|
324
|
+
% in, a shorter name E ends with (File.delete may be
|
|
325
|
+
% java.io.File.delete), a longer name ending in E at a segment
|
|
326
|
+
% boundary (checkNotNull may be named by
|
|
327
|
+
% com.google.common.base.Preconditions.checkNotNull), and *.m
|
|
328
|
+
% when E ends in .m (it may be E).
|
|
329
|
+
% includes E holds only on a member that certainly is E: E itself, a
|
|
330
|
+
% member under the prefix of E*, or a member ending in .m for *.m.
|
|
331
|
+
% *.m never satisfies X.m.
|
|
332
|
+
% subset_of A member is allowed only when it certainly is an allowed item:
|
|
333
|
+
% the item itself, a member under the prefix of an item E*, or a
|
|
334
|
+
% member ending in .m for *.m. *.m is allowed only when *.m is.
|
|
335
|
+
% eq Compares the spellings exactly, as for every other reader.
|
|
336
|
+
|
|
337
|
+
source_matching(Certificate, references(Files)) :-
|
|
338
|
+
get_dict(source, Certificate, Source),
|
|
339
|
+
is_dict(Source),
|
|
340
|
+
get_dict(reader, Source, Reader),
|
|
341
|
+
text_string(Reader, "references"),
|
|
342
|
+
!,
|
|
343
|
+
( get_dict(files, Source, RawFiles)
|
|
344
|
+
-> ( is_list(RawFiles) -> maplist(text_string, RawFiles, Files) ; text_string(RawFiles, File), Files = [File] )
|
|
345
|
+
; Files = []
|
|
346
|
+
).
|
|
347
|
+
source_matching(_, literal).
|
|
348
|
+
|
|
349
|
+
member_separator(".").
|
|
350
|
+
member_separator("/").
|
|
351
|
+
member_separator("::").
|
|
352
|
+
|
|
353
|
+
% extends_member(+Member, +Stem): Member is Stem or lies under it at a
|
|
354
|
+
% segment boundary. The empty stem (from the item "*") covers everything.
|
|
355
|
+
extends_member(_, "") :- !.
|
|
356
|
+
extends_member(Member, Stem) :- Member == Stem, !.
|
|
357
|
+
extends_member(Member, Stem) :-
|
|
358
|
+
member_separator(Separator),
|
|
359
|
+
string_concat(Stem, Separator, Prefix),
|
|
360
|
+
string_concat(Prefix, _, Member),
|
|
361
|
+
!.
|
|
362
|
+
|
|
363
|
+
% pattern_stem(+Item, -Stem): Item ends in *; Stem drops it and the separator
|
|
364
|
+
% before it (java.sql.* -> java.sql, * -> "").
|
|
365
|
+
pattern_stem(Item, Stem) :-
|
|
366
|
+
\+ unknown_receiver(Item, _),
|
|
367
|
+
string_concat(Base, "*", Item),
|
|
368
|
+
( member_separator(Separator), string_concat(Stem0, Separator, Base)
|
|
369
|
+
-> Stem = Stem0
|
|
370
|
+
; Stem = Base
|
|
371
|
+
).
|
|
372
|
+
|
|
373
|
+
% unknown_receiver(+Member, -Method): Member is *.Method.
|
|
374
|
+
unknown_receiver(Member, Method) :-
|
|
375
|
+
string_concat("*.", Method, Member),
|
|
376
|
+
Method \== "".
|
|
377
|
+
|
|
378
|
+
% ends_with_method(+Member, +Method): Member's last segment is Method.
|
|
379
|
+
ends_with_method(Member, Method) :-
|
|
380
|
+
member_separator(Separator),
|
|
381
|
+
string_concat(Separator, Method, Suffix),
|
|
382
|
+
string_concat(_, Suffix, Member),
|
|
383
|
+
!.
|
|
384
|
+
|
|
385
|
+
% exclude_match(+Actual, +Expected, -How): Actual is (certain) or may be
|
|
386
|
+
% (possible) what Expected names.
|
|
387
|
+
exclude_match(Actual, Expected, certain) :- Actual == Expected, !.
|
|
388
|
+
exclude_match(Actual, Expected, certain) :-
|
|
389
|
+
unknown_receiver(Expected, Method),
|
|
390
|
+
\+ unknown_receiver(Actual, _),
|
|
391
|
+
ends_with_method(Actual, Method),
|
|
392
|
+
!.
|
|
393
|
+
exclude_match(Actual, Expected, possible) :-
|
|
394
|
+
unknown_receiver(Actual, Method),
|
|
395
|
+
!,
|
|
396
|
+
( pattern_stem(Expected, _)
|
|
397
|
+
; ends_with_method(Expected, Method)
|
|
398
|
+
),
|
|
399
|
+
!.
|
|
400
|
+
exclude_match(Actual, Expected, How) :-
|
|
401
|
+
pattern_stem(Actual, ActualStem),
|
|
402
|
+
!,
|
|
403
|
+
( pattern_stem(Expected, Target) -> true ; Target = Expected ),
|
|
404
|
+
( extends_member(ActualStem, Target)
|
|
405
|
+
-> How = certain
|
|
406
|
+
; extends_member(Target, ActualStem)
|
|
407
|
+
-> How = possible
|
|
408
|
+
).
|
|
409
|
+
exclude_match(Actual, Expected, How) :-
|
|
410
|
+
( pattern_stem(Expected, Stem)
|
|
411
|
+
-> extends_member(Actual, Stem),
|
|
412
|
+
How = certain
|
|
413
|
+
; extends_member(Actual, Expected)
|
|
414
|
+
-> How = certain
|
|
415
|
+
; ends_with_method(Expected, Actual)
|
|
416
|
+
-> How = possible
|
|
417
|
+
; ends_with_method(Actual, Expected)
|
|
418
|
+
-> How = possible
|
|
419
|
+
).
|
|
420
|
+
|
|
421
|
+
% certain_match(+Actual, +Expected): Actual certainly is what Expected names
|
|
422
|
+
% (includes, subset_of).
|
|
423
|
+
certain_match(Actual, Expected) :- Actual == Expected, !.
|
|
424
|
+
certain_match(_, "*") :- !.
|
|
425
|
+
certain_match(Actual, Expected) :-
|
|
426
|
+
\+ unknown_receiver(Actual, _),
|
|
427
|
+
( pattern_stem(Expected, Stem)
|
|
428
|
+
-> ( pattern_stem(Actual, ActualStem)
|
|
429
|
+
-> extends_member(ActualStem, Stem)
|
|
430
|
+
; extends_member(Actual, Stem)
|
|
431
|
+
)
|
|
432
|
+
; unknown_receiver(Expected, Method)
|
|
433
|
+
-> \+ pattern_stem(Actual, _),
|
|
434
|
+
ends_with_method(Actual, Method)
|
|
435
|
+
),
|
|
436
|
+
!.
|
|
437
|
+
|
|
438
|
+
files_text([], "the files read") :- !.
|
|
439
|
+
files_text(Files, Text) :-
|
|
440
|
+
length(Files, N),
|
|
441
|
+
( N > 3
|
|
442
|
+
-> length(Shown, 3), append(Shown, _, Files),
|
|
443
|
+
atomic_list_concat(Shown, ', ', Listed),
|
|
444
|
+
Extra is N - 3,
|
|
445
|
+
format(string(Text), "~w and ~w more", [Listed, Extra])
|
|
446
|
+
; atomic_list_concat(Files, ', ', Text)
|
|
447
|
+
).
|
|
448
|
+
|
|
449
|
+
excluded_text(Files, Actual-Expected-How, Text) :-
|
|
450
|
+
( How == certain, Actual == Expected
|
|
451
|
+
-> Text = Actual
|
|
452
|
+
; How == certain
|
|
453
|
+
-> format(string(Text), "~w (within ~w)", [Actual, Expected])
|
|
454
|
+
; unknown_receiver(Actual, Method)
|
|
455
|
+
-> files_text(Files, Where),
|
|
456
|
+
format(string(Text), "~w (a call of ~w on a receiver Kibi cannot name, in ~w; it may be ~w)", [Actual, Method, Where, Expected])
|
|
457
|
+
; pattern_stem(Actual, _)
|
|
458
|
+
-> format(string(Text), "~w (a wildcard that may cover ~w)", [Actual, Expected])
|
|
459
|
+
; ends_with_method(Actual, Expected)
|
|
460
|
+
-> format(string(Text), "~w (it may be the ~w the fact excludes)", [Actual, Expected])
|
|
461
|
+
; format(string(Text), "~w (a shorter name that may be ~w)", [Actual, Expected])
|
|
462
|
+
).
|
|
463
|
+
|
|
464
|
+
% reference_relation_problem(+Operator, +Actual, +Expected, +Files, +What,
|
|
465
|
+
% -Reason) fails when the relation holds under the reference matching above.
|
|
466
|
+
reference_relation_problem("excludes", Actual, Expected, Files, What, Reason) :-
|
|
467
|
+
!,
|
|
468
|
+
findall(A-E-How, (member(A, Actual), member(E, Expected), exclude_match(A, E, How)), Pairs0),
|
|
469
|
+
sort(Pairs0, Pairs),
|
|
470
|
+
Pairs \== [],
|
|
471
|
+
include(certain_pair, Pairs, Certain),
|
|
472
|
+
maplist(excluded_text(Files), Pairs, Texts),
|
|
473
|
+
atomic_list_concat(Texts, ', ', Text),
|
|
474
|
+
( Certain \== []
|
|
475
|
+
-> format(string(Reason), "~w include ~w, which the fact excludes", [What, Text])
|
|
476
|
+
; format(string(Reason), "~w include ~w, which the fact excludes; Kibi cannot decide the claim for those members", [What, Text])
|
|
477
|
+
).
|
|
478
|
+
reference_relation_problem("includes", Actual, Expected, _, What, Reason) :-
|
|
479
|
+
!,
|
|
480
|
+
exclude(matched_by_some(Actual), Expected, Missing),
|
|
481
|
+
Missing \== [],
|
|
482
|
+
atomic_list_concat(Missing, ', ', Text),
|
|
483
|
+
format(string(Reason), "~w do not include ~w", [What, Text]).
|
|
484
|
+
reference_relation_problem("subset_of", Actual, Expected, _, What, Reason) :-
|
|
485
|
+
!,
|
|
486
|
+
exclude(allowed_by_some(Expected), Actual, Extra),
|
|
487
|
+
Extra \== [],
|
|
488
|
+
atomic_list_concat(Extra, ', ', Text),
|
|
489
|
+
( member(Member, Extra), unknown_receiver(Member, _)
|
|
490
|
+
-> format(string(Reason), "~w include ~w, which the fact's set does not allow (a call on a receiver Kibi cannot name is allowed only when the set lists it as *.m)", [What, Text])
|
|
491
|
+
; format(string(Reason), "~w include ~w, which the fact's set does not allow", [What, Text])
|
|
492
|
+
).
|
|
493
|
+
reference_relation_problem(Operator, Actual, Expected, _, What, Reason) :-
|
|
494
|
+
relation_problem(Operator, Actual, Expected, What, Reason).
|
|
495
|
+
|
|
496
|
+
certain_pair(_-_-certain).
|
|
497
|
+
|
|
498
|
+
matched_by_some(Actual, Expected) :-
|
|
499
|
+
member(Member, Actual),
|
|
500
|
+
certain_match(Member, Expected),
|
|
501
|
+
!.
|
|
502
|
+
|
|
503
|
+
allowed_by_some(Expected, Member) :-
|
|
504
|
+
member(Item, Expected),
|
|
505
|
+
certain_match(Member, Item),
|
|
506
|
+
!.
|
|
507
|
+
|
|
309
508
|
% ---------------------------------------------------------------------------
|
|
310
509
|
% Obligations
|
|
311
510
|
|
|
@@ -397,14 +596,18 @@ obligation_result("machine_relation", Obligation, model(_, _, Machine), Result)
|
|
|
397
596
|
; Result = ok
|
|
398
597
|
)
|
|
399
598
|
).
|
|
400
|
-
obligation_result("set_relation", Obligation, model(_, _, source(Actual, _, _, _)), Result) :- !,
|
|
599
|
+
obligation_result("set_relation", Obligation, model(_, _, source(Actual, _, _, _, Matching)), Result) :- !,
|
|
401
600
|
dict_text(Obligation, operator, Operator),
|
|
402
601
|
obligation_strings(Obligation, Expected),
|
|
403
|
-
|
|
602
|
+
What = "the members Kibi read from the source",
|
|
603
|
+
( ( Matching = references(Files)
|
|
604
|
+
-> reference_relation_problem(Operator, Actual, Expected, Files, What, Reason)
|
|
605
|
+
; relation_problem(Operator, Actual, Expected, What, Reason)
|
|
606
|
+
)
|
|
404
607
|
-> Result = problem(Reason)
|
|
405
608
|
; Result = ok
|
|
406
609
|
).
|
|
407
|
-
obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _, _)), Result) :-
|
|
610
|
+
obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _, _, _)), Result) :-
|
|
408
611
|
memberchk(FieldKind, ["required_fields", "nullable_fields"]),
|
|
409
612
|
!,
|
|
410
613
|
( is_list(Entities), Entities \== []
|
|
@@ -417,7 +620,7 @@ obligation_result(FieldKind, Obligation, model(_, _, source(_, Entities, _, _)),
|
|
|
417
620
|
-> Result = problem(Reason)
|
|
418
621
|
; Result = ok
|
|
419
622
|
).
|
|
420
|
-
obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Routes, _)), Result) :- !,
|
|
623
|
+
obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Routes, _, _)), Result) :- !,
|
|
421
624
|
( is_list(Routes), Routes \== []
|
|
422
625
|
-> true
|
|
423
626
|
; throw(model_error("the source has no routes; read a route table for a middleware claim"))
|
|
@@ -436,7 +639,7 @@ obligation_result("route_middleware", Obligation, model(_, _, source(_, _, Route
|
|
|
436
639
|
; Result = ok
|
|
437
640
|
).
|
|
438
641
|
% implements REQ-kibi-source-proof-kinds
|
|
439
|
-
obligation_result("sequence_precedes", Obligation, model(_, _, source(_, _, _, Sequence)), Result) :- !,
|
|
642
|
+
obligation_result("sequence_precedes", Obligation, model(_, _, source(_, _, _, Sequence, _)), Result) :- !,
|
|
440
643
|
( is_list(Sequence)
|
|
441
644
|
-> true
|
|
442
645
|
; throw(model_error("the source has no order; read a list for a precedes claim"))
|