From d8a8c7d103fc4d10ebdec62abd15635315226a9f Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 4 Nov 2022 17:24:01 +0100 Subject: [PATCH 01/13] lint: check approaches and articles There is not yet a formal spec for an approach `config.json` file or an article `config.json` file, but I've tried to implement linting them from the docs for approaches [1][2] and a commit [3] on the csharp track. [1] https://github.com/exercism/docs/blob/5509b2f12fac/building/tracks/concept-exercises.md#file-approachesconfigjson [2] https://github.com/exercism/docs/blob/5509b2f12fac/building/tracks/practice-exercises.md#file-approachesconfigjson [3] https://github.com/exercism/csharp/commit/4069cca97782 --- src/lint/approaches_and_articles.nim | 83 ++++++++++++++++++++++++++++ src/lint/lint.nim | 5 +- 2 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/lint/approaches_and_articles.nim diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim new file mode 100644 index 00000000..cff519a8 --- /dev/null +++ b/src/lint/approaches_and_articles.nim @@ -0,0 +1,83 @@ +import std/[json, strformat, strutils] +import ".."/helpers +import "."/validators + +type + DirKind = enum + dkApproaches = "approaches" + dkArticles = "articles" + +proc hasValidIntroduction(data: JsonNode, path: Path): bool = + const k = "introduction" + if hasObject(data, k, path): + let d = data[k] + let checks = [ + hasArrayOfStrings(d, "authors", path, k, uniqueValues = true), + hasArrayOfStrings(d, "contributors", path, k, isRequired = false), + ] + result = allTrue(checks) + +proc isValidApproachOrArticle(data: JsonNode, context: string, + path: Path): bool = + if isObject(data, context, path): + let checks = [ + hasString(data, "uuid", path, context, checkIsUuid = true), + hasString(data, "slug", path, context, checkIsKebab = true), + hasString(data, "title", path, context, maxLen = 255), + hasString(data, "blurb", path, context, maxLen = 280), + hasArrayOfStrings(data, "authors", path, context, uniqueValues = true), + hasArrayOfStrings(data, "contributors", path, context, + isRequired = false), + ] + result = allTrue(checks) + +proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = + if isObject(data, jsonRoot, path): + let checks = [ + if dk == dkApproaches: hasValidIntroduction(data, path) else: true, + hasArrayOf(data, $dk, path, isValidApproachOrArticle, isRequired = false), + ] + result = allTrue(checks) + +proc isConfigMissingOrValid(dir: Path, dk: DirKind): bool = + result = true + let configPath = dir / &".{dk}" / "config.json" + if fileExists(configPath): + let j = parseJsonFile(configPath, result) + if j != nil: + if not isValidConfig(j, configPath, dk): + result = false + +proc isEverySnippetValid(exerciseDir: Path, dk: DirKind): bool = + result = true + for dir in getSortedSubdirs(exerciseDir / &".{dk}"): + let snippetPath = block: + let ext = if dk == dkApproaches: "txt" else: "md" + dir / &"snippet.{ext}" + if fileExists(snippetPath): + let contents = readFile(snippetPath) + var numLines = 0 + for line in contents.splitLines(): + if not (line.startsWith("```") and dk == dkArticles): + inc numLines + dec numLines # Allow 8 lines with a final newline. + const maxNumLines = 8 + if numLines > maxNumLines: + let msg = &"The file is {numLines} lines long, but it must be at " & + &"most {maxNumLines} lines long" + result.setFalseAndPrint(msg, snippetPath) + +proc isDirValid(exerciseDir: Path): bool = + result = true + for dk in DirKind: + if not isConfigMissingOrValid(exerciseDir, dk): + result = false + if not isEverySnippetValid(exerciseDir, dk): + result = false + +proc isEveryApproachAndArticleValid*(trackDir: Path): bool = + result = true + for exerciseKind in ["concept", "practice"]: + for exerciseDir in getSortedSubdirs(trackDir / "exercises" / exerciseKind): + if not isDirValid(exerciseDir): + result = false diff --git a/src/lint/lint.nim b/src/lint/lint.nim index dd4f5804..35dfc3ce 100644 --- a/src/lint/lint.nim +++ b/src/lint/lint.nim @@ -1,7 +1,7 @@ import std/[strformat, strutils] import ".."/[cli, helpers] -import "."/[concept_exercises, concepts, docs, practice_exercises, - track_config, validators] +import "."/[approaches_and_articles, concept_exercises, concepts, docs, + practice_exercises, track_config, validators] proc allChecksPass(trackDir: Path): bool = ## Returns true if all the linting checks pass for the track at `trackDir`. @@ -16,6 +16,7 @@ proc allChecksPass(trackDir: Path): bool = isEveryConceptConfigValid(trackDir), isEveryConceptExerciseConfigValid(trackDir), isEveryPracticeExerciseConfigValid(trackDir), + isEveryApproachAndArticleValid(trackDir), sharedExerciseDocsExist(trackDir), trackDocsExist(trackDir), ] From 0e70d2b14f80d93c6df762477f1bab6836805b06 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 4 Nov 2022 17:50:01 +0100 Subject: [PATCH 02/13] lint(approaches_and_articles): inline `isDirValid` --- src/lint/approaches_and_articles.nim | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index cff519a8..7fbdeb16 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -67,17 +67,12 @@ proc isEverySnippetValid(exerciseDir: Path, dk: DirKind): bool = &"most {maxNumLines} lines long" result.setFalseAndPrint(msg, snippetPath) -proc isDirValid(exerciseDir: Path): bool = - result = true - for dk in DirKind: - if not isConfigMissingOrValid(exerciseDir, dk): - result = false - if not isEverySnippetValid(exerciseDir, dk): - result = false - proc isEveryApproachAndArticleValid*(trackDir: Path): bool = result = true for exerciseKind in ["concept", "practice"]: for exerciseDir in getSortedSubdirs(trackDir / "exercises" / exerciseKind): - if not isDirValid(exerciseDir): - result = false + for dk in DirKind: + if not isConfigMissingOrValid(exerciseDir, dk): + result = false + if not isEverySnippetValid(exerciseDir, dk): + result = false From d79953b4149ae7c566690b1405760d9777cb9b44 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 4 Nov 2022 19:03:01 +0100 Subject: [PATCH 03/13] clint(approaches_and_articles): add dots to `DirKind` strings --- src/lint/approaches_and_articles.nim | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index 7fbdeb16..fe3564ef 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -4,8 +4,8 @@ import "."/validators type DirKind = enum - dkApproaches = "approaches" - dkArticles = "articles" + dkApproaches = ".approaches" + dkArticles = ".articles" proc hasValidIntroduction(data: JsonNode, path: Path): bool = const k = "introduction" @@ -33,15 +33,16 @@ proc isValidApproachOrArticle(data: JsonNode, context: string, proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = if isObject(data, jsonRoot, path): + let k = dk.`$`[1..^1] # Remove dot. let checks = [ if dk == dkApproaches: hasValidIntroduction(data, path) else: true, - hasArrayOf(data, $dk, path, isValidApproachOrArticle, isRequired = false), + hasArrayOf(data, k, path, isValidApproachOrArticle, isRequired = false), ] result = allTrue(checks) proc isConfigMissingOrValid(dir: Path, dk: DirKind): bool = result = true - let configPath = dir / &".{dk}" / "config.json" + let configPath = dir / $dk / "config.json" if fileExists(configPath): let j = parseJsonFile(configPath, result) if j != nil: @@ -50,7 +51,7 @@ proc isConfigMissingOrValid(dir: Path, dk: DirKind): bool = proc isEverySnippetValid(exerciseDir: Path, dk: DirKind): bool = result = true - for dir in getSortedSubdirs(exerciseDir / &".{dk}"): + for dir in getSortedSubdirs(exerciseDir / $dk): let snippetPath = block: let ext = if dk == dkApproaches: "txt" else: "md" dir / &"snippet.{ext}" From 6cc4380de777ba380ec2294d5bd3f3a654692d7d Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Wed, 9 Nov 2022 13:39:01 +0100 Subject: [PATCH 04/13] lint(approaches_and_articles): error for 8 newlines then non-newline Make `configlet lint` produce an error for a snippet with 8 newline characters then non-newline content on the 9th line, e.g.: 1\n2\n3\n4\n5\n6\n7\n8\n9 Note that this means we do not count lines like `wc`: $ printf '1\n2\n3\n4\n5\n6\n7\n8' | wc -l 7 $ printf '1\n2\n3\n4\n5\n6\n7\n8\n' | wc -l 8 $ printf '1\n2\n3\n4\n5\n6\n7\n8\n9' | wc -l 8 $ printf '1\n2\n3\n4\n5\n6\n7\n8\n9\n' | wc -l 9 --- src/lint/approaches_and_articles.nim | 25 ++++++---- tests/test_lint.nim | 69 +++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index fe3564ef..b4c8eb8f 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -49,6 +49,19 @@ proc isConfigMissingOrValid(dir: Path, dk: DirKind): bool = if not isValidConfig(j, configPath, dk): result = false +func countLinesWithoutCodeFence(s: string, dk: DirKind): int = + ## Returns the number of lines in `s`, but: + ## + ## - excluding lines that open or close a Markdown code fence. + ## - including a final line that does not end in a newline character. + result = 0 + if s.len > 0: + for line in s.splitLines(): + if not (line.startsWith("```") and dk == dkArticles): + inc result + if s[^1] in ['\n', '\l']: + dec result + proc isEverySnippetValid(exerciseDir: Path, dk: DirKind): bool = result = true for dir in getSortedSubdirs(exerciseDir / $dk): @@ -57,15 +70,11 @@ proc isEverySnippetValid(exerciseDir: Path, dk: DirKind): bool = dir / &"snippet.{ext}" if fileExists(snippetPath): let contents = readFile(snippetPath) - var numLines = 0 - for line in contents.splitLines(): - if not (line.startsWith("```") and dk == dkArticles): - inc numLines - dec numLines # Allow 8 lines with a final newline. - const maxNumLines = 8 - if numLines > maxNumLines: + const maxLines = 8 + let numLines = countLinesWithoutCodeFence(contents, dk) + if numLines > maxLines: let msg = &"The file is {numLines} lines long, but it must be at " & - &"most {maxNumLines} lines long" + &"most {maxLines} lines long" result.setFalseAndPrint(msg, snippetPath) proc isEveryApproachAndArticleValid*(trackDir: Path): bool = diff --git a/tests/test_lint.nim b/tests/test_lint.nim index 925f7a59..83d42c3f 100644 --- a/tests/test_lint.nim +++ b/tests/test_lint.nim @@ -1,5 +1,6 @@ -import std/unittest +import std/[strutils, unittest] import "."/lint/validators +import "."/lint/approaches_and_articles {.all.} proc testIsKebabCase = suite "isKebabCase": @@ -279,11 +280,77 @@ proc testIsFilesPattern = isFilesPattern("somedir/%{pascal_slug}/%{snake_slug}.suffix") isFilesPattern("%{pascal_slug}/filename.suffix") +proc testCountLinesWithoutCodeFence = + suite "countLinesWithoutCodeFence": + test "without code fence": + const dk = dkArticles + check: + countLinesWithoutCodeFence("", dk) == 0 + countLinesWithoutCodeFence("a", dk) == 1 + countLinesWithoutCodeFence("a\n", dk) == 1 + countLinesWithoutCodeFence("foo\n", dk) == 1 + countLinesWithoutCodeFence("foo\nb", dk) == 2 + countLinesWithoutCodeFence("foo\nbar", dk) == 2 + countLinesWithoutCodeFence("foo\nbar\n", dk) == 2 + countLinesWithoutCodeFence("foo\nbar\nfoo", dk) == 3 + + test "with code fence only": + const s = """ + ```nim + echo "foo" + ```""".unindent() + check: + countLinesWithoutCodeFence(s, dkArticles) == 1 + countLinesWithoutCodeFence(s, dkApproaches) == 3 + + test "with code fence at start": + const s = """ + ```nim + echo "foo" + ``` + + Some content. + """.unindent() + check: + countLinesWithoutCodeFence(s, dkArticles) == 3 + countLinesWithoutCodeFence(s, dkApproaches) == 5 + + test "with code fence at end": + const s = """ + # Some header + + Some content. + + ```nim + echo "foo" + ``` + """.unindent() + check: + countLinesWithoutCodeFence(s, dkArticles) == 5 + countLinesWithoutCodeFence(s, dkApproaches) == 7 + + test "with code fence in middle": + const s = """ + # Some header + + Some content. + + ```nim + echo "foo" + ``` + + Some content. + """.unindent() + check: + countLinesWithoutCodeFence(s, dkArticles) == 7 + countLinesWithoutCodeFence(s, dkApproaches) == 9 + proc main = testIsKebabCase() testIsUuidV4() testExtractPlaceholders() testIsFilesPattern() + testCountLinesWithoutCodeFence() main() {.used.} From 3adba39d7d538822f19994adac75d4b23f2b3dee Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Wed, 9 Nov 2022 16:36:01 +0100 Subject: [PATCH 05/13] lint(approaches_and_articles): do not error for missing `introduction` --- src/lint/approaches_and_articles.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index b4c8eb8f..eafbfe83 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -9,7 +9,7 @@ type proc hasValidIntroduction(data: JsonNode, path: Path): bool = const k = "introduction" - if hasObject(data, k, path): + if hasObject(data, k, path, isRequired = false): let d = data[k] let checks = [ hasArrayOfStrings(d, "authors", path, k, uniqueValues = true), From a260510691ad7efad30d69bb1ce48ca8bae09ef3 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:36:02 +0100 Subject: [PATCH 06/13] lint(articles_and_approaches): error for missing `config.json` --- src/lint/approaches_and_articles.nim | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index eafbfe83..880624b7 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -42,12 +42,22 @@ proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = proc isConfigMissingOrValid(dir: Path, dk: DirKind): bool = result = true - let configPath = dir / $dk / "config.json" + let dkPath = dir / $dk + let configPath = dkPath / "config.json" if fileExists(configPath): let j = parseJsonFile(configPath, result) if j != nil: if not isValidConfig(j, configPath, dk): result = false + else: + if dk == dkApproaches and fileExists(dkPath / "introduction.md"): + let msg = &"The below directory has an 'introduction.md' file, but " & + "does not contain a 'config.json' file" + result.setFalseAndPrint(msg, dkPath) + for dir in getSortedSubdirs(dkPath, relative = true): + let msg = &"The below directory has a '{dir}' subdirectory, but does " & + "not contain a 'config.json' file" + result.setFalseAndPrint(msg, dkPath) func countLinesWithoutCodeFence(s: string, dk: DirKind): int = ## Returns the number of lines in `s`, but: From 6747ce1cddae367916175ab8611b2413165e2644 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:36:03 +0100 Subject: [PATCH 07/13] lint: print that approaches/articles were linted --- src/lint/lint.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lint/lint.nim b/src/lint/lint.nim index 35dfc3ce..a2f39a0f 100644 --- a/src/lint/lint.nim +++ b/src/lint/lint.nim @@ -46,6 +46,7 @@ proc lint*(conf: Conf) = - Every concept exercise has a valid .meta/config.json file - Every practice exercise has the required .md files - Every practice exercise has a valid .meta/config.json file + - Every approach and article is valid - Required track docs are present - Required shared exercise docs are present""".dedent() if printedWarning: From c5ee39ee356c7f2c7aac1047ebe2f7cb189f435e Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Thu, 10 Nov 2022 16:36:04 +0100 Subject: [PATCH 08/13] lint(approaches_and_articles), helpers: error for missing slug dir --- src/helpers.nim | 1 + src/lint/approaches_and_articles.nim | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/helpers.nim b/src/helpers.nim index 53ca746c..37b49a2b 100644 --- a/src/helpers.nim +++ b/src/helpers.nim @@ -92,6 +92,7 @@ proc dirExists*(path: Path): bool {.borrow.} proc fileExists*(path: Path): bool {.borrow.} proc readFile*(path: Path): string {.borrow.} proc writeFile*(path: Path; content: string) {.borrow.} +proc parentDir*(path: Path): string {.borrow.} func toLineAndCol(s: string; offset: Natural): tuple[line: int; col: int] = ## Returns the line and column number corresponding to the `offset` in `s`. diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index 880624b7..2028c38f 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -1,4 +1,4 @@ -import std/[json, strformat, strutils] +import std/[json, os, strformat, strutils] import ".."/helpers import "."/validators @@ -30,6 +30,13 @@ proc isValidApproachOrArticle(data: JsonNode, context: string, isRequired = false), ] result = allTrue(checks) + if result: + let slug = data["slug"].getStr() + let slugDir = path.parentDir() / slug + if not dirExists(slugDir): + let msg = &"A 'slug' value is '{slug}', but there is no sibling " & + "directory with that name" + result.setFalseAndPrint(msg, path) proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = if isObject(data, jsonRoot, path): From 0300ae6a43deb73429ee2de81982d14c5cf27307 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 11 Nov 2022 13:56:01 +0100 Subject: [PATCH 09/13] lint(approaches_and_articles): improve msg for missing dir --- src/lint/approaches_and_articles.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index 2028c38f..37addd63 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -32,11 +32,11 @@ proc isValidApproachOrArticle(data: JsonNode, context: string, result = allTrue(checks) if result: let slug = data["slug"].getStr() - let slugDir = path.parentDir() / slug + let slugDir = Path(path.parentDir() / slug) if not dirExists(slugDir): - let msg = &"A 'slug' value is '{slug}', but there is no sibling " & - "directory with that name" - result.setFalseAndPrint(msg, path) + let msg = &"A config.json '{context}.slug' value is '{slug}', but " & + "there is no corresponding directory at the below location" + result.setFalseAndPrint(msg, slugDir) proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = if isObject(data, jsonRoot, path): From fe94cf28a9a1f8d5e445ef006bd034fee7c49768 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 11 Nov 2022 13:56:02 +0100 Subject: [PATCH 10/13] lint(approaches_and_articles): error for missing intro/content/snippet Try to implement these checks for `.approaches/config.json` - If the `introduction.authors` array is non-empty, there must be a non-empty `introduction.md` file - Any `approaches.slug` value must have a corresponding non-empty `/content.md` file - Any `approaches.slug` value must have a corresponding non-empty `/snippet.txt` file And these checks for `.articles/config.json` - Any `articles.slug` value must have a corresponding non-empty `/content.md` file - Any `articles.slug` value must have a corresponding non-empty `/snippet.md` file Some output: $ configlet lint [...] The config.json 'introduction' object is present, but there is no corresponding introduction file at the below location: ./exercises/practice/bob/.approaches/introduction.md A config.json 'approaches.slug' value is 'if', but there is no corresponding content file at the below location: ./exercises/practice/bob/.approaches/if/content.md A config.json 'approaches.slug' value is 'answer-array', but there is no corresponding snippet file at the below location: ./exercises/practice/bob/.approaches/answer-array/snippet.txt A config.json 'articles.slug' value is 'performance', but there is no corresponding content file at the below location: ./exercises/practice/bob/.articles/performance/content.md A config.json 'articles.slug' value is 'performance', but there is no corresponding snippet file at the below location: ./exercises/practice/bob/.articles/performance/snippet.md --- src/lint/approaches_and_articles.nim | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index 37addd63..ad69d7dd 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -7,6 +7,14 @@ type dkApproaches = ".approaches" dkArticles = ".articles" +proc setFalseIfFileMissingOrEmpty(b: var bool, path: Path, msgMissing: string) = + if fileExists(path): + if path.readFile().len == 0: + let msg = &"The below file is empty" + b.setFalseAndPrint(msg, path) + else: + b.setFalseAndPrint(msgMissing, path) + proc hasValidIntroduction(data: JsonNode, path: Path): bool = const k = "introduction" if hasObject(data, k, path, isRequired = false): @@ -16,6 +24,11 @@ proc hasValidIntroduction(data: JsonNode, path: Path): bool = hasArrayOfStrings(d, "contributors", path, k, isRequired = false), ] result = allTrue(checks) + if result and data.hasKey(k): + let introductionPath = Path(path.parentDir() / "introduction.md") + let msg = &"The config.json '{k}' object is present, but there is no " & + "corresponding introduction file at the below location" + result.setFalseIfFileMissingOrEmpty(introductionPath, msg) proc isValidApproachOrArticle(data: JsonNode, context: string, path: Path): bool = @@ -32,11 +45,23 @@ proc isValidApproachOrArticle(data: JsonNode, context: string, result = allTrue(checks) if result: let slug = data["slug"].getStr() - let slugDir = Path(path.parentDir() / slug) + let dkDir = path.parentDir() + let slugDir = Path(dkDir / slug) if not dirExists(slugDir): let msg = &"A config.json '{context}.slug' value is '{slug}', but " & "there is no corresponding directory at the below location" result.setFalseAndPrint(msg, slugDir) + block: + let contentPath = slugDir / "content.md" + let msg = &"A config.json '{context}.slug' value is '{slug}', but " & + "there is no corresponding content file at the below location" + result.setFalseIfFileMissingOrEmpty(contentPath, msg) + block: + let ext = if dkDir.endsWith($dkApproaches): "txt" else: "md" + let snippetPath = slugDir / &"snippet.{ext}" + let msg = &"A config.json '{context}.slug' value is '{slug}', but " & + "there is no corresponding snippet file at the below location" + result.setFalseIfFileMissingOrEmpty(snippetPath, msg) proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = if isObject(data, jsonRoot, path): From 5e710b26bc4f56a6738eeaab8b430c355af56a7b Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 11 Nov 2022 15:20:01 +0100 Subject: [PATCH 11/13] lint(approaches_and_articles): remove an `if result` --- src/lint/approaches_and_articles.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index ad69d7dd..fc8e2347 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -24,7 +24,7 @@ proc hasValidIntroduction(data: JsonNode, path: Path): bool = hasArrayOfStrings(d, "contributors", path, k, isRequired = false), ] result = allTrue(checks) - if result and data.hasKey(k): + if data.kind == JObject and data.hasKey(k): let introductionPath = Path(path.parentDir() / "introduction.md") let msg = &"The config.json '{k}' object is present, but there is no " & "corresponding introduction file at the below location" From abe6d593cc1e80894f4a10de4223c68efa2fc6a9 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 11 Nov 2022 15:20:02 +0100 Subject: [PATCH 12/13] lint(approaches_and_articles): error for dir not in config slugs Example output: $ configlet lint [...] There is no 'approaches.slug' key with the value 'performance', but a sibling directory exists with that name: ../exercism-tracks/csharp/exercises/practice/reverse-string/.approaches/config.json --- src/lint/approaches_and_articles.nim | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index fc8e2347..42fe9a6b 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -63,6 +63,15 @@ proc isValidApproachOrArticle(data: JsonNode, context: string, "there is no corresponding snippet file at the below location" result.setFalseIfFileMissingOrEmpty(snippetPath, msg) +proc getSlugs(data: JsonNode, k: string): seq[string] = + result = @[] + if data.kind == JObject and data.hasKey(k): + if data[k].kind == JArray: + let elems = data[k].getElems() + for e in elems: + if e.kind == JObject and e.hasKey("slug") and e["slug"].kind == JString: + result.add e["slug"].getStr() + proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = if isObject(data, jsonRoot, path): let k = dk.`$`[1..^1] # Remove dot. @@ -71,6 +80,13 @@ proc isValidConfig(data: JsonNode, path: Path, dk: DirKind): bool = hasArrayOf(data, k, path, isValidApproachOrArticle, isRequired = false), ] result = allTrue(checks) + if result: + let slugsInConfig = getSlugs(data, k) + for dir in getSortedSubdirs(path.parentDir().Path, relative = true): + if dir.string notin slugsInConfig: + let msg = &"There is no '{k}.slug' key with the value '{dir}', " & + &"but a sibling directory exists with that name" + result.setFalseAndPrint(msg, path) proc isConfigMissingOrValid(dir: Path, dk: DirKind): bool = result = true From fe35e8631a662047afffd04efa3035cf3ebfb1ca Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 11 Nov 2022 17:59:01 +0100 Subject: [PATCH 13/13] lint(approaches_and_articles): fix whitespace Appease nimpretty. --- src/lint/approaches_and_articles.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lint/approaches_and_articles.nim b/src/lint/approaches_and_articles.nim index 42fe9a6b..2ddd8d48 100644 --- a/src/lint/approaches_and_articles.nim +++ b/src/lint/approaches_and_articles.nim @@ -99,8 +99,8 @@ proc isConfigMissingOrValid(dir: Path, dk: DirKind): bool = result = false else: if dk == dkApproaches and fileExists(dkPath / "introduction.md"): - let msg = &"The below directory has an 'introduction.md' file, but " & - "does not contain a 'config.json' file" + let msg = &"The below directory has an 'introduction.md' file, but " & + "does not contain a 'config.json' file" result.setFalseAndPrint(msg, dkPath) for dir in getSortedSubdirs(dkPath, relative = true): let msg = &"The below directory has a '{dir}' subdirectory, but does " &