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.
@@ -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
- ( relation_problem(Operator, Actual, Expected, "the members Kibi read from the source", Reason)
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"))