Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 125 additions & 51 deletions compiler/graindoc/docblock.re
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ open Grain_typed;
open Grain_utils;
open Grain_diagnostics;

module StringSet = Set.Make(String);

type param = {
param_id: string,
param_type: string,
Expand Down Expand Up @@ -110,13 +112,18 @@ type error =
})
| MissingLabeledParamType({name: string})
| MissingUnlabeledParamType({idx: int})
| DuplicateLabelParamType({name: string})
| MissingReturnType
| AttributeAppearsMultipleTimes({attr: string})
| InvalidAttribute({
name: string,
attr: string,
});

type argument =
| LabeledArg(string)
| UnlabeledArg(int);

exception Error(Location.t, error);

let report_error = (ppf, err) => {
Expand All @@ -141,6 +148,12 @@ let report_error = (ppf, err) => {
"Unable to find a type for parameter at index %d. Make sure a parameter exists at this index in the parameter list.",
idx,
)
| DuplicateLabelParamType({name}) =>
Format.fprintf(
ppf,
"Multiple `@param` attributes found for parameter %s. Each parameter may only have one associated `@param` attribute.",
name,
)
| MissingReturnType =>
Format.fprintf(ppf, "Unable to find a return type. Please file an issue!")
| AttributeAppearsMultipleTimes({attr}) =>
Expand Down Expand Up @@ -406,11 +419,119 @@ let for_value_description =
};

let (args, return_type) = types_for_function(~ident, vd);
let args = Option.value(args, ~default=[]);

// Extract parameter documentation from attributes
let param_attributes: list((argument, (string, Location.t, ref(bool)))) =
List.filter_map(
(attr: Comment_attributes.t) =>
switch (attr.attr) {
| Comment_attributes.Param({attr_id, attr_desc}) =>
switch (attr_id) {
| PositionalParam(idx, loc) =>
Some((UnlabeledArg(idx), (attr_desc, loc, ref(false))))
| LabeledParam(name, loc) =>
Some((LabeledArg(name), (attr_desc, loc, ref(false))))
}
| _ => None
},
attributes,
);
let has_param_attributes = List.length(param_attributes) > 0;

let match_label_to_attr = (label: argument) => {
switch (List.assoc_opt(label, param_attributes)) {
| None =>
Location.prerr_warning(
loc,
Grain_utils.Warnings.ParameterDocumentationMissing(
switch (label) {
| LabeledArg(name) => name
| UnlabeledArg(idx) => string_of_int(idx)
},
),
);
("", Location.dummy_loc);
| Some((attr_desc, attr_loc, used)) =>
used := true;
(attr_desc, attr_loc);
};
};

// Precompute parameter documentation
let documented_params =
if (has_param_attributes) {
List.mapi(
(index, (label: Asttypes.argument_label, typ: Types.type_expr)) => {
let (param_id, typ, (param_msg, param_loc)) =
switch (label, typ) {
| (Unlabeled, _) => (
string_of_int(index),
typ,
match_label_to_attr(UnlabeledArg(index)),
)
| (Labeled(l), _) => (
l.txt,
typ,
match_label_to_attr(LabeledArg(l.txt)),
)
| (Default(l), {desc: TTyConstr(_, [typ], _)}) => (
"?" ++ l.txt,
typ,
match_label_to_attr(LabeledArg(l.txt)),
)
| (Default(_), _) =>
failwith("Impossible: Default parameter type is not an Option")
};

{
param_id,
param_type: Printtyp.string_of_type_sch(typ),
param_msg,
param_loc,
};
},
args,
);
} else {
[];
};

// Validate that all parameter attributes were used (i.e. no invalid labels, no duplicates)
ignore @@
List.fold_left(
(acc, (label, (_, loc, used))) => {
let label_name =
switch (label) {
| LabeledArg(name) => name
| UnlabeledArg(idx) => string_of_int(idx)
};
if (used^ == false) {
raise(
Error(
loc,
if (StringSet.mem(label_name, acc)) {
DuplicateLabelParamType({name: label_name});
} else {
switch (label) {
| LabeledArg(name) => MissingLabeledParamType({name: name})
| UnlabeledArg(idx) => MissingUnlabeledParamType({idx: idx})
};
},
),
);
};
StringSet.add(label_name, acc);
},
StringSet.empty,
param_attributes,
);

// Map attributes to their respective fields
let (deprecations, since, history, params, returns, throws, examples) =
List.fold_left(
(
(deprecations, since, history, params, returns, throws, examples),
(deprecations, since, history, params, returns, throws, examples) as acc,
{attr, attr_loc}: Comment_attributes.t,
) => {
switch (attr) {
Expand Down Expand Up @@ -467,55 +588,8 @@ let for_value_description =
throws,
examples,
)
| Param({attr_id: param_id, attr_desc: param_msg}) =>
let (param_id, param_type) =
switch (param_id) {
| PositionalParam(idx, _) =>
switch (lookup_type_expr(~idx, args)) {
| Some((_, typ)) => (
string_of_int(idx),
Printtyp.string_of_type_sch(typ),
)
| None =>
raise(
Error(attr_loc, MissingUnlabeledParamType({idx: idx})),
)
}
| LabeledParam(name, _) =>
switch (lookup_arg_by_label(name, args)) {
| Some((Labeled(_), typ)) => (
name,
Printtyp.string_of_type_sch(typ),
)
// Default parameters have the type Option<a>; extract the type from the Option
| Some((Default(_), {desc: TTyConstr(_, [typ], _)})) => (
"?" ++ name,
Printtyp.string_of_type_sch(typ),
)
| _ =>
raise(
Error(attr_loc, MissingLabeledParamType({name: name})),
)
}
};

(
deprecations,
since,
history,
[
{
param_id,
param_type,
param_msg,
param_loc: attr_loc,
},
...params,
],
returns,
throws,
examples,
);
// Do nothing because param documentation is precomputed
| Param(_) => acc
| Returns({attr_desc: returns_msg}) =>
switch (returns) {
| Some(_) =>
Expand Down Expand Up @@ -579,7 +653,7 @@ let for_value_description =
}
},
// deprecations, since, history, params, returns, throws, examples
([], None, [], [], None, [], []),
([], None, [], List.rev(documented_params), None, [], []),
attributes,
);

Expand Down
13 changes: 9 additions & 4 deletions compiler/src/utils/warnings.re
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ type t =
| UselessRecordSpread
| PrintUnsafe(string)
| ToStringUnsafe(string)
| ArrayIndexNonInteger(string);
| ArrayIndexNonInteger(string)
| ParameterDocumentationMissing(string);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to add this as a warning to the compiler proper. We should start a set of Graindoc warnings.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a noted in discord until #2343, I don't think there's an easy way todo this while using the same handler and printing logic we use in the compiler. I think it's ideal we use the same printing logic so any printing improvements there get reflected back in the grain doc warnings.

Maybe for #2342 the warning handler could be a functor that takes the error types and printing handler allowing us to use it in all of our tooling.

Copy link
Copy Markdown
Member

@spotandjake spotandjake Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a noted in discord until #2343, I don't think there's an easy way todo this while using the same handler and printing logic we use in the compiler. I think it's ideal we use the same printing logic so any printing improvements there get reflected back in the grain doc warnings.

Maybe for #2342 the warning handler could be a functor that takes the error types and printing handler allowing us to use it in all of our tooling.

Unless you have a better idea I think we should keep the error handling here for now and move with #2342, we can renumber our errors then which shouldn't be a problem as we don't reference the numbers anywhere currently.


let last_warning_number = 22;
let last_warning_number = 23;

let number =
fun
Expand All @@ -69,7 +70,8 @@ let number =
| UselessRecordSpread => 19
| PrintUnsafe(_) => 20
| ToStringUnsafe(_) => 21
| ArrayIndexNonInteger(_) => last_warning_number;
| ArrayIndexNonInteger(_) => 22
| ParameterDocumentationMissing(_) => last_warning_number;

let message =
fun
Expand Down Expand Up @@ -175,7 +177,9 @@ let message =
++ typ
++ " from the `runtime/debugPrint` module instead."
| ArrayIndexNonInteger(idx) =>
"Array index should be an integer, but found `" ++ idx ++ "`.";
"Array index should be an integer, but found `" ++ idx ++ "`."
| ParameterDocumentationMissing(param) =>
"Missing documentation for parameter `" ++ param ++ "`.";

let sub_locs =
fun
Expand Down Expand Up @@ -223,6 +227,7 @@ let defaults = [
PrintUnsafe(""),
ToStringUnsafe(""),
ArrayIndexNonInteger(""),
ParameterDocumentationMissing(""),
];

let _ = List.iter(x => current^.active[number(x)] = true, defaults);
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/utils/warnings.rei
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ type t =
| UselessRecordSpread
| PrintUnsafe(string)
| ToStringUnsafe(string)
| ArrayIndexNonInteger(string);
| ArrayIndexNonInteger(string)
| ParameterDocumentationMissing(string);

let is_active: t => bool;
let is_error: t => bool;
Expand Down
7 changes: 7 additions & 0 deletions compiler/test/graindoc/paramDuplicate.input.gr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ParamDuplicate

/**
* @param a: This is the first
* @param a: This is a duplicate
*/
provide let test1 = a => a + a
6 changes: 6 additions & 0 deletions compiler/test/graindoc/paramUnknown.input.gr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module ParamUnknown

/**
* @param x: This parameter is documented
*/
provide let test2 = a => a
93 changes: 93 additions & 0 deletions compiler/test/graindoc/params.expected.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: Params
---

## Values

Functions and constants included in the Params module.

### Params.**test1**

```grain
test1: (a: Number, b: Number) => Number
```

Fully documented labeled parameters test.

Parameters:

| param | type | description |
| ----- | -------- | ------------------ |
| `a` | `Number` | This is the first |
| `b` | `Number` | This is the second |

### Params.**test2**

```grain
test2: ((Number, a), (Number, b)) => Number
```

Fully documented unlabeled parameters test.

Parameters:

| param | type | description |
| ----- | ------------- | ------------------ |
| `0` | `(Number, a)` | This is the first |
| `1` | `(Number, b)` | This is the second |

### Params.**test3**

```grain
test3: (a: Number, (Number, a), c: Number) => Number
```

Fully documented mixed parameters test.

Parameters:

| param | type | description |
| ----- | ------------- | ------------------ |
| `a` | `Number` | This is the first |
| `1` | `(Number, a)` | This is the second |
| `c` | `Number` | This is the third |

### Params.**test4**

```grain
test4: (a: Number, ?b: Number) => Number
```

Optional parameters test.

Parameters:

| param | type | description |
| ----- | -------- | ------------- |
| `a` | `Number` | This is the a |
| `?b` | `Number` | This is the b |

### Params.**test5**

```grain
test5: (a: Number, b: Number) => Number
```

No documentation test.

### Params.**test6**

```grain
test6: (a: Number, b: Number, c: Number) => Number
```

Partially documented parameters test.

Parameters:

| param | type | description |
| ----- | -------- | ------------- |
| `a` | `Number` | This is the a |
| `b` | `Number` | |
| `c` | `Number` | This is the c |

Loading