|
| 1 | +/** |
| 2 | + * @name Missing QLDoc for parameter |
| 3 | + * @description It is suspicious when a predicate has a parameter that is |
| 4 | + * unmentioned in the qldoc, and the qldoc contains a |
| 5 | + * code-fragment mentioning a non-parameter. |
| 6 | + * @kind problem |
| 7 | + * @problem.severity warning |
| 8 | + * @precision high |
| 9 | + * @id ql/missing-parameter-qldoc |
| 10 | + * @tags maintainability |
| 11 | + */ |
| 12 | + |
| 13 | +import ql |
| 14 | + |
| 15 | +/** |
| 16 | + * Gets a fragment enclosed in backticks (`) from a QLDoc of the predicate `p`. |
| 17 | + * Skips code-blocks that are triple-quoted. |
| 18 | + */ |
| 19 | +private string getACodeFragment(Predicate p) { |
| 20 | + result = p.getQLDoc().getContents().regexpFind("`([^`]*)`(?!`)", _, _) |
| 21 | +} |
| 22 | + |
| 23 | +/** Gets a parameter name from `p`. */ |
| 24 | +private string getAParameterName(Predicate p) { result = p.getParameter(_).getName() } |
| 25 | + |
| 26 | +/** |
| 27 | + * Gets the name of a parameter of `p` that is mentioned in any way in the QLDoc of `p`. |
| 28 | + * Also includes names that are mentioned in non-code fragments. |
| 29 | + */ |
| 30 | +private string getADocumentedParameter(Predicate p) { |
| 31 | + result = p.getQLDoc().getContents().regexpFind("\\b\\w[\\w_]*\\b", _, _) and |
| 32 | + result.toLowerCase() = getAParameterName(p).toLowerCase() |
| 33 | +} |
| 34 | + |
| 35 | +/** |
| 36 | + * Get something that looks like a parameter name from the QLDoc, |
| 37 | + * but which is not a parameter of `p`. |
| 38 | + */ |
| 39 | +private string getAMentionedNonParameter(Predicate p) { |
| 40 | + exists(string fragment | fragment = getACodeFragment(p) | |
| 41 | + result = fragment.substring(1, fragment.length() - 1) |
| 42 | + ) and |
| 43 | + result.regexpMatch("^[a-z]\\w+$") and |
| 44 | + not result.toLowerCase() = getAParameterName(p).toLowerCase() and |
| 45 | + not result = ["true", "false", "NaN"] and // keywords |
| 46 | + not result.regexpMatch("\\d+") and // numbers |
| 47 | + // predicates get mentioned all the time, it's fine. |
| 48 | + not result = |
| 49 | + any(Predicate pred | pred.getLocation().getFile() = p.getLocation().getFile()).getName() and |
| 50 | + // classes get mentioned all the time, it's fine. |
| 51 | + not result = |
| 52 | + any(TypeExpr t | t.getLocation().getFile() = p.getLocation().getFile()) |
| 53 | + .getResolvedType() |
| 54 | + .getName() |
| 55 | +} |
| 56 | + |
| 57 | +/** Gets a parameter name from `p` that is not mentioned in the qldoc. */ |
| 58 | +private string getAnUndocumentedParameter(Predicate p) { |
| 59 | + result = getAParameterName(p) and |
| 60 | + not result.toLowerCase() = getADocumentedParameter(p).toLowerCase() and |
| 61 | + not result = ["config", "conf", "cfg"] // DataFlow configurations are often undocumented, and that's fine. |
| 62 | +} |
| 63 | + |
| 64 | +/** Holds if `p` has documented parameters, but `param` is undocumented */ |
| 65 | +private predicate missingDocumentation(Predicate p, string param) { |
| 66 | + param = getAnUndocumentedParameter(p) and |
| 67 | + exists(getADocumentedParameter(p)) |
| 68 | +} |
| 69 | + |
| 70 | +/** Gets the one string containing the undocumented parameters from `p` */ |
| 71 | +private string getUndocumentedParameters(Predicate p) { |
| 72 | + result = strictconcat(string param | missingDocumentation(p, param) | param, ", or ") |
| 73 | +} |
| 74 | + |
| 75 | +/** Gets the parameter-like names mentioned in the QLDoc of `p` that are not parameters. */ |
| 76 | +private string getMentionedNonParameters(Predicate p) { |
| 77 | + result = strictconcat(string param | param = getAMentionedNonParameter(p) | param, ", and ") |
| 78 | +} |
| 79 | + |
| 80 | +from Predicate p |
| 81 | +select p, |
| 82 | + "The QLDoc has no documentation for " + getUndocumentedParameters(p) + ", but the QLDoc mentions " |
| 83 | + + getMentionedNonParameters(p) |
0 commit comments