Skip to content

Commit 0ba7cdf

Browse files
authored
Enforce consistent punctuation in algorithms (graphql#1069)
* Consistent punctuation in algorithms * Clarify style guide * Boolean formatting * Add a format check to CI
1 parent 8682a86 commit 0ba7cdf

9 files changed

+184
-45
lines changed

.github/algorithm-format-check.mjs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { readFile, readdir } from "node:fs/promises";
2+
3+
const SPEC_DIR = new URL("../spec", import.meta.url).pathname;
4+
5+
process.exitCode = 0;
6+
const filenames = await readdir(SPEC_DIR);
7+
for (const filename of filenames) {
8+
if (!filename.endsWith(".md")) {
9+
continue;
10+
}
11+
const markdown = await readFile(`${SPEC_DIR}/${filename}`, "utf8");
12+
13+
/**
14+
* Not strictly 'lines' since we try and group indented things together as if
15+
* they were one line. Close enough though.
16+
*/
17+
const lines = markdown.split(/\n(?=[\S\n]|\s*(?:-|[0-9]+\.) )/);
18+
19+
for (let i = 0, l = lines.length; i < l; i++) {
20+
const line = lines[i];
21+
22+
// Check algorithm is consistently formatted
23+
{
24+
// Is it an algorithm definition?
25+
const matches = line.match(/^([a-z0-9A-Z]+)(\s*)\(([^)]*)\)(\s*):(\s*)$/);
26+
if (matches) {
27+
const [, algorithmName, ns1, _args, ns2, ns3] = matches;
28+
if (ns1 || ns2 || ns3) {
29+
console.log(
30+
`Bad whitespace in definition of ${algorithmName} in '${filename}':`
31+
);
32+
console.log(line);
33+
console.log();
34+
process.exitCode = 1;
35+
}
36+
if (lines[i + 1] !== "") {
37+
console.log(
38+
`No empty space after algorithm ${algorithmName} header in '${filename}'`
39+
);
40+
console.log();
41+
process.exitCode = 1;
42+
}
43+
for (let j = i + 2; j < l; j++) {
44+
const step = lines[j];
45+
if (!step.match(/^\s*(-|[0-9]+\.) /)) {
46+
if (step !== "") {
47+
console.log(
48+
`Bad algorithm ${algorithmName} step in '${filename}':`
49+
);
50+
console.log(step);
51+
console.log();
52+
process.exitCode = 1;
53+
}
54+
break;
55+
}
56+
if (!step.match(/[.:]$/)) {
57+
console.log(
58+
`Bad formatting for '${algorithmName}' step (does not end in '.' or ':') in '${filename}':`
59+
);
60+
console.log(step);
61+
console.log();
62+
process.exitCode = 1;
63+
}
64+
if (step.match(/^\s*(-|[0-9]\.)\s+[a-z]/)) {
65+
console.log(
66+
`Bad formatting of '${algorithmName}' step (should start with a capital) in '${filename}':`
67+
);
68+
console.log(step);
69+
console.log();
70+
process.exitCode = 1;
71+
}
72+
const trimmedInnerLine = step.replace(/\s+/g, " ");
73+
if (
74+
trimmedInnerLine.match(
75+
/(?:[rR]eturn|is (?:not )?)(true|false|null)\b/
76+
) &&
77+
!trimmedInnerLine.match(/null or empty/)
78+
) {
79+
console.log(
80+
`Potential bad formatting of '${algorithmName}' step (true/false/null should be wrapped in curly braces, e.g. '{true}') in '${filename}':`
81+
);
82+
console.log(step);
83+
console.log();
84+
process.exitCode = 1;
85+
}
86+
}
87+
}
88+
}
89+
90+
// Check `- ...:` step is followed by an indent
91+
{
92+
const matches = line.match(/^(\s*)- .*:\s*$/);
93+
if (matches) {
94+
const indent = matches[1];
95+
const nextLine = lines[i + 1];
96+
if (!nextLine.startsWith(`${indent} `)) {
97+
console.log(
98+
`Lacking indent in '${filename}' following ':' character:`
99+
);
100+
console.dir(line);
101+
console.dir(nextLine);
102+
console.log();
103+
// TODO: process.exitCode = 1;
104+
}
105+
}
106+
}
107+
}
108+
}
109+
110+
if (process.exitCode === 0) {
111+
console.log(`Everything looks okay!`);
112+
} else {
113+
console.log(`Please resolve the errors detailed above.`);
114+
}

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
- uses: actions/setup-node@v3
2222
- run: npm ci
2323
- run: npm run test:format
24+
- run: npm run test:algorithm-format
2425
test-build:
2526
runs-on: ubuntu-latest
2627
steps:

STYLE_GUIDE.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,29 @@ All elements in hyphenated words follow the same rules, e.g. headings may
5656
contain `Non-Null`, `Context-Free`, `Built-in` (`in` is a preposition, so is not
5757
capitalized).
5858

59-
## Lists
59+
## Algorithms
6060

61-
Lists can be written as full sentences or as fragments. Algorithms that appear
62-
as lists, however, should be written in full sentences with proper punctuation.
61+
A named algorithm definition starts with the name of the algorithm in
62+
`PascalCase`, an open parenthesis, a comma-and-space separated list of
63+
arguments, a close parenthesis and then a colon. It is followed by a blank
64+
newline and a list of steps in the algorithm which may be numbered or bulleted.
65+
66+
Each step in an algorithm should either end in a colon (`:`) with an indented
67+
step on the next line, or a fullstop (`.`). (A step after a step ending in a
68+
full stop may or may not be indented, use your discretion.)
69+
70+
Indentation in algorithms is significant.
71+
72+
Every step in an algorithm should start with a capital letter.
73+
74+
```
75+
MyAlgorithm(argOne, argTwo):
76+
77+
- Let {something} be {true}.
78+
- For each {arg} in {argOne}:
79+
- If {arg} is greater than {argTwo}:
80+
- Let {something} be {false}.
81+
- Otherwise if {arg} is less than {argTwo}:
82+
- Let {something} be {true}.
83+
- Return {something}.
84+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"test:spelling": "cspell \"spec/**/*.md\" README.md",
1818
"format": "prettier --write \"**/*.{md,yml,yaml,json}\"",
1919
"test:format": "prettier --check \"**/*.{md,yml,yaml,json}\" || npm run suggest:format",
20+
"test:algorithm-format": "node .github/algorithm-format-check.mjs",
2021
"suggest:format": "echo \"\nTo resolve this, run: $(tput bold)npm run format$(tput sgr0)\" && exit 1",
2122
"build": "./build.sh",
2223
"test:build": "spec-md --metadata spec/metadata.json spec/GraphQL.md > /dev/null",

spec/Section 2 -- Language.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ BlockStringValue(rawValue):
10321032
- Let {lines} be the result of splitting {rawValue} by {LineTerminator}.
10331033
- Let {commonIndent} be {null}.
10341034
- For each {line} in {lines}:
1035-
- If {line} is the first item in {lines}, continue to the next line.
1035+
- If {line} is the first item in {lines}, continue to the next {line}.
10361036
- Let {length} be the number of characters in {line}.
10371037
- Let {indent} be the number of leading consecutive {WhiteSpace} characters in
10381038
{line}.
@@ -1117,7 +1117,7 @@ ListValue : [ ]
11171117
ListValue : [ Value+ ]
11181118

11191119
- Let {inputList} be a new empty list value.
1120-
- For each {Value+}
1120+
- For each {Value+}:
11211121
- Let {value} be the result of evaluating {Value}.
11221122
- Append {value} to {inputList}.
11231123
- Return {inputList}.
@@ -1164,7 +1164,7 @@ ObjectValue : { }
11641164
ObjectValue : { ObjectField+ }
11651165

11661166
- Let {inputObject} be a new input object value with no fields.
1167-
- For each {field} in {ObjectField+}
1167+
- For each {field} in {ObjectField+}:
11681168
- Let {name} be {Name} in {field}.
11691169
- Let {value} be the result of evaluating {Value} in {field}.
11701170
- Add a field to {inputObject} of name {name} containing value {value}.
@@ -1247,22 +1247,22 @@ input type.
12471247

12481248
Type : Name
12491249

1250-
- Let {name} be the string value of {Name}
1250+
- Let {name} be the string value of {Name}.
12511251
- Let {type} be the type defined in the Schema named {name}
1252-
- {type} must not be {null}
1253-
- Return {type}
1252+
- {type} must not be {null}.
1253+
- Return {type}.
12541254

12551255
Type : [ Type ]
12561256

1257-
- Let {itemType} be the result of evaluating {Type}
1257+
- Let {itemType} be the result of evaluating {Type}.
12581258
- Let {type} be a List type where {itemType} is the contained type.
1259-
- Return {type}
1259+
- Return {type}.
12601260

12611261
Type : Type !
12621262

1263-
- Let {nullableType} be the result of evaluating {Type}
1263+
- Let {nullableType} be the result of evaluating {Type}.
12641264
- Let {type} be a Non-Null type where {nullableType} is the contained type.
1265-
- Return {type}
1265+
- Return {type}.
12661266

12671267
## Directives
12681268

spec/Section 3 -- Type System.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -347,22 +347,22 @@ can only be used as input types. Object, Interface, and Union types can only be
347347
used as output types. Lists and Non-Null types may be used as input types or
348348
output types depending on how the wrapped type may be used.
349349

350-
IsInputType(type) :
350+
IsInputType(type):
351351

352352
- If {type} is a List type or Non-Null type:
353353
- Let {unwrappedType} be the unwrapped type of {type}.
354-
- Return IsInputType({unwrappedType})
354+
- Return IsInputType({unwrappedType}).
355355
- If {type} is a Scalar, Enum, or Input Object type:
356-
- Return {true}
356+
- Return {true}.
357357
- Return {false}.
358358

359-
IsOutputType(type) :
359+
IsOutputType(type):
360360

361361
- If {type} is a List type or Non-Null type:
362362
- Let {unwrappedType} be the unwrapped type of {type}.
363-
- Return IsOutputType({unwrappedType})
363+
- Return IsOutputType({unwrappedType}).
364364
- If {type} is a Scalar, Object, Interface, Union, or Enum type:
365-
- Return {true}
365+
- Return {true}.
366366
- Return {false}.
367367

368368
### Type Extensions
@@ -919,7 +919,7 @@ of rules must be adhered to by every Object type in a GraphQL schema.
919919
3. The argument must accept a type where {IsInputType(argumentType)}
920920
returns {true}.
921921
4. If argument type is Non-Null and a default value is not defined:
922-
- The `@deprecated` directive must not be applied to this argument.
922+
1. The `@deprecated` directive must not be applied to this argument.
923923
3. An object type may declare that it implements one or more unique interfaces.
924924
4. An object type must be a super-set of all interfaces it implements:
925925
1. Let this object type be {objectType}.
@@ -1699,7 +1699,7 @@ input ExampleInputObject {
16991699
3. The input field must accept a type where {IsInputType(inputFieldType)}
17001700
returns {true}.
17011701
4. If input field type is Non-Null and a default value is not defined:
1702-
- The `@deprecated` directive must not be applied to this input field.
1702+
1. The `@deprecated` directive must not be applied to this input field.
17031703
3. If an Input Object references itself either directly or through referenced
17041704
Input Objects, at least one of the fields in the chain of references must be
17051705
either a nullable or a List type.

spec/Section 4 -- Introspection.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,8 @@ The `__Field` type represents each field in an Object or Interface type.
414414

415415
Fields\:
416416

417-
- `name` must return a String
418-
- `description` may return a String or {null}
417+
- `name` must return a String.
418+
- `description` may return a String or {null}.
419419
- `args` returns a List of `__InputValue` representing the arguments this field
420420
accepts.
421421
- Accepts the argument `includeDeprecated` which defaults to {false}. If
@@ -433,8 +433,8 @@ The `__InputValue` type represents field and directive arguments as well as the
433433

434434
Fields\:
435435

436-
- `name` must return a String
437-
- `description` may return a String or {null}
436+
- `name` must return a String.
437+
- `description` may return a String or {null}.
438438
- `type` must return a `__Type` that represents the type this input value
439439
expects.
440440
- `defaultValue` may return a String encoding (using the GraphQL language) of
@@ -451,8 +451,8 @@ The `__EnumValue` type represents one of possible values of an enum.
451451

452452
Fields\:
453453

454-
- `name` must return a String
455-
- `description` may return a String or {null}
454+
- `name` must return a String.
455+
- `description` may return a String or {null}.
456456
- `isDeprecated` returns {true} if this enum value should no longer be used,
457457
otherwise {false}.
458458
- `deprecationReason` optionally provides a reason why this enum value is
@@ -489,8 +489,8 @@ supported. All possible locations are listed in the `__DirectiveLocation` enum:
489489

490490
Fields\:
491491

492-
- `name` must return a String
493-
- `description` may return a String or {null}
492+
- `name` must return a String.
493+
- `description` may return a String or {null}.
494494
- `locations` returns a List of `__DirectiveLocation` representing the valid
495495
locations this directive may be placed.
496496
- `args` returns a List of `__InputValue` representing the arguments this

spec/Section 5 -- Validation.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,25 +435,25 @@ SameResponseShape(fieldA, fieldB):
435435
- Let {typeA} be the return type of {fieldA}.
436436
- Let {typeB} be the return type of {fieldB}.
437437
- If {typeA} or {typeB} is Non-Null:
438-
- If {typeA} or {typeB} is nullable, return false.
438+
- If {typeA} or {typeB} is nullable, return {false}.
439439
- Let {typeA} be the nullable type of {typeA}.
440440
- Let {typeB} be the nullable type of {typeB}.
441441
- If {typeA} or {typeB} is List:
442-
- If {typeA} or {typeB} is not List, return false.
442+
- If {typeA} or {typeB} is not List, return {false}.
443443
- Let {typeA} be the item type of {typeA}.
444444
- Let {typeB} be the item type of {typeB}.
445445
- Repeat from step 3.
446446
- If {typeA} or {typeB} is Scalar or Enum:
447-
- If {typeA} and {typeB} are the same type return true, otherwise return
448-
false.
447+
- If {typeA} and {typeB} are the same type return {true}, otherwise return
448+
{false}.
449449
- Assert: {typeA} and {typeB} are both composite types.
450450
- Let {mergedSet} be the result of adding the selection set of {fieldA} and the
451451
selection set of {fieldB}.
452452
- Let {fieldsForName} be the set of selections with a given response name in
453453
{mergedSet} including visiting fragments and inline fragments.
454454
- Given each pair of members {subfieldA} and {subfieldB} in {fieldsForName}:
455-
- If {SameResponseShape(subfieldA, subfieldB)} is false, return false.
456-
- Return true.
455+
- If {SameResponseShape(subfieldA, subfieldB)} is {false}, return {false}.
456+
- Return {true}.
457457

458458
**Explanatory Text**
459459

0 commit comments

Comments
 (0)