123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265openAstopenTyping_env(* The [Suggestion] diagnostics the quick-fix pass emits (redundant annotations,
compound assignment, field punning). Kept with their emitters; the message
combinator is the same trivial [Wax_utils.Message] alias the typer's [Error]
uses. *)moduleError=structopenWax_utils(* A machine-applicable simplification (severity [Suggestion]), carrying the
rewrite in [edit] for editor quick fixes and shown by [wax check] only when
its [warning] is enabled. Marked [universal] so path-sensitive checking
reports it once; suppressed in recovery mode like [warn]. *)letsuggest~warning~editcontext~locationmessage=ifnot(Wax_utils.Diagnostic.in_recoverycontext)thenDiagnostic.reportcontext~location~severity:Suggestion~warning~universal:true~edit~message()end(* Locate the ': t' type annotation in the source between a binding name's end
([name_end]) and the boundary after it ([boundary] — the initializer, the next
binding, or the tuple close). The AST keeps no span for the annotation, so it
is recovered from the source: skip past the ':', then scan to the first of
[,] [=] [;] [)] []] that terminates it. This is safe because every annotation
position parses a [value_type] (see [value_type] in parser.mly) — an
identifier or [&[?][!]ident] — a short token sequence that never contains any
of those characters or a bracket (an inline [&fn(...)] type exists only in
cast position, never here). Returns a pair: the type's own span (to underline)
and the ': t' span to delete (from the ':' to the type's end, so a comment
before it is kept). [None] when the annotation spans several lines or cannot
be isolated. *)letannotation_spansctx(name_end:Lexing.position)(boundary:Lexing.position)=ifname_end.pos_lnum<>boundary.pos_lnumthenNoneelsematchsource_slicectx(spanname_endboundary)with|None->None|Somegap->((* Blank comments so a ':' or terminator inside one is not mistaken for
the annotation's. *)letgap=blank_commentsgapinmatchString.index_optgap':'with|None->None|Somecolon->letn=String.lengthgapinletstop=letrecscani=ifi>=nthennelsematchgap.[i]with|')'|']'|','|'='|';'->i|_->scan(i+1)inscan(colon+1)inletis_wsc=c=' '||c='\t'inlets=ref(colon+1)ande=refstopinwhile!s<!e&&is_wsgap.[!s]doincrsdone;while!e>!s&&is_wsgap.[!e-1]dodecredone;if!s>=!ethenNoneelseletatoff={name_endwithpos_cnum=name_end.pos_cnum+off}inSome(span(at!s)(at!e),span(atcolon)(at!e)))(* Suggest dropping a binding's redundant type annotation — the ': t' that the
initializer's inferred type already pins ([let x: t = e] -> [let x = e], and
likewise for each binding of a tuple [let] and a redundant global annotation).
The span is recovered by [annotation_spans]; the edit deletes the ': t',
underlining just the type. *)letsuggest_redundant_annotationctx~name_end~boundary=matchannotation_spansctxname_endboundarywith|Some(type_span,delete_span)->Error.suggest~warning:Wax_utils.Warning.Redundant_annotation~edit:(deletion_editdelete_span)ctx.diagnostics~location:type_span(Wax_utils.Message.text"This type annotation is redundant; the initializer's type is \
inferred.")|None->()(* The binary operators that have a compound-assignment form [x op= e] (the
arithmetic and bitwise ones); comparisons are excluded. Mirrors the parser's
[compound_assign_op]. *)letcompound_assignable=function|Add|Sub|Mul|Div_|Rem_|And|Or|Xor|Shl|Shr_->true|Eq|Ne|Lt_|Gt_|Le_|Ge_->false(* Suggest rewriting a plain assignment [x = x op e] as the compound form
[x op= e]. Only fires when the left operand is [x] itself (so [x = e - x] is
left alone, since it is not [x -= e]). The replacement is spliced from the
source: the target, the operator's own span (which already excludes the '='),
and the right operand — so any comment or spacing inside [e] is preserved. *)letsuggest_compound_assignmentctx~locationidx(rhs_expr:_instr)=matchrhs_expr.descwith|BinOp(op,lhs,rhs)whencompound_assignableop.desc&&matchlhs.descwithGetg->g.desc=idx.Annot.desc|_->false->(match(source_slicectxidx.info,source_slicectxop.info,source_slicectxrhs.info)with|Sometarget,Someopstr,Somerhs_src->Error.suggest~warning:Wax_utils.Warning.Compound_assignment~edit:{Wax_utils.Diagnostic.edit_location=location;new_text=Printf.sprintf"%s %s= %s"targetopstrrhs_src;}ctx.diagnostics~location(Wax_utils.Message.text(Printf.sprintf"This assignment can use the compound form '%s %s= …'."targetopstr))|_->())|_->()(* Suggest the punning shorthand [{x}] for a field written explicitly as [x: x]
(its value is [Get x] for the like-named local/global). Deletes the ': x' that
runs from the field name's end to the value's end. *)letsuggest_punningctx(name:ident)written=matchwrittenwith|Some({desc=Getg;_}asvalue)wheng.desc=name.desc->Error.suggest~warning:Wax_utils.Warning.Field_punning~edit:(deletion_edit(spanname.info.loc_endvalue.info.loc_end))ctx.diagnostics~location:name.info(Wax_utils.Message.text(Printf.sprintf"This field can use the punning shorthand '%s'."name.desc))|_->()(* Suggest dropping a construction's redundant type name — the [T] in a struct
[{T| …}] or array [{T| …}] literal that the fields / expected type already
pin. The name-less surface form omits the [T|] separator, so the edit deletes
from the name's start through the following [|] (found by a short source scan,
bailing out if a newline intervenes). *)letsuggest_drop_type_namectx(name:ident)=matchWax_utils.Diagnostic.sourcectx.diagnosticswith|None->()|Somesrc->letn=String.lengthsrcinleti=refname.info.loc_end.pos_cnuminwhile!i<n&&(src.[!i]=' '||src.[!i]='\t')doincridone;if!i<n&&src.[!i]='|'thenletafter={name.info.loc_endwithpos_cnum=!i+1}inError.suggest~warning:Wax_utils.Warning.Redundant_annotation~edit:(deletion_edit(spanname.info.loc_startafter))ctx.diagnostics~location:name.info(Wax_utils.Message.text"This type name is redundant; it is inferred here.")(* Suggest dropping a block's redundant result type — the [t] in [do t { … }] /
[loop t { … }] / [try t { … }] that the context already pins. A block
expression takes no params, so the result is a single bare type between the
keyword and the [{]. The AST keeps no span for it, so it is found in the
source: locate the (reserved, hence unambiguous) keyword as a whole word, then
take the type up to the brace. Comments are blanked first (so a keyword inside
one is not matched), and it bails unless keyword and brace are on one line, so
a wrong edit is never produced. *)letsuggest_block_resultctx~keyword(block_start:Lexing.position)(brace_start:Lexing.position)=matchifblock_start.pos_lnum<>brace_start.pos_lnumthenNoneelseOption.mapblank_comments(source_slicectx(spanblock_startbrace_start))with|None->()|Someprefix->(letis_idc=(c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')||c='_'inletm=String.lengthkeywordandn=String.lengthprefixinletrecfindi=ifi+m>nthenNoneelseifString.subprefixim=keyword&&(i=0||not(is_idprefix.[i-1]))&&(i+m=n||not(is_idprefix.[i+m]))thenSomeielsefind(i+1)inmatchfind0with|None->()|Somek->letis_wsc=c=' '||c='\t'inlets=ref(k+m)ande=refninwhile!s<n&&is_wsprefix.[!s]doincrsdone;while!e>!s&&is_wsprefix.[!e-1]dodecredone;if!s<!ethenletatoff={block_startwithpos_cnum=block_start.pos_cnum+off}inError.suggest~warning:Wax_utils.Warning.Redundant_annotation(* Delete just the type token, so a comment before the [{] is kept;
the formatter tidies the leftover spacing. *)~edit:(deletion_edit(span(at!s)(at!e)))ctx.diagnostics~location:(span(at!s)(at!e))(Wax_utils.Message.text"This result type is redundant; it is inferred from the \
context."))(* Suggest dropping an [if]'s redundant result type — the [=> t] between the
condition and the [{] that the context already pins (the [if]-expression
analogue of [suggest_block_result]). The AST keeps no span for it, so it is
found in the source between the condition's end and the brace: locate the
[=>], then take the type up to the brace. Comments are blanked first and it
bails unless [=>] and brace are on one line. The edit deletes the whole
[=> t]. *)letsuggest_if_resultctx(cond_end:Lexing.position)(brace_start:Lexing.position)=matchifcond_end.pos_lnum<>brace_start.pos_lnumthenNoneelseOption.mapblank_comments(source_slicectx(spancond_endbrace_start))with|None->()|Somegap->(letn=String.lengthgapinletrecfindi=ifi+1>=nthenNoneelseifgap.[i]='='&&gap.[i+1]='>'thenSomeielsefind(i+1)inmatchfind0with|None->()|Somearrow->letis_wsc=c=' '||c='\t'inlets=ref(arrow+2)ande=refninwhile!s<n&&is_wsgap.[!s]doincrsdone;while!e>!s&&is_wsgap.[!e-1]dodecredone;if!s<!ethenletatoff={cond_endwithpos_cnum=cond_end.pos_cnum+off}inError.suggest~warning:Wax_utils.Warning.Redundant_annotation(* Delete the whole '=> t'; the formatter tidies the spacing. *)~edit:(deletion_edit(span(atarrow)(at!e)))ctx.diagnostics~location:(span(at!s)(at!e))(Wax_utils.Message.text"This result type is redundant; it is inferred from the \
context."))