From 7439d03166330a7d21c85d98f5680760bfafe177 Mon Sep 17 00:00:00 2001 From: Frans Bothma <52746596+Steffe-Dev@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:52:20 +0200 Subject: [PATCH 01/13] [Resistor Duo analyzer (#127)] Add feedback to extract the constant to the top level if defined locally (#164) * [Resistor Duo analyzer] Add feedback to extract the constant to the top level if defined locally * [Resistor Duo analyzer] Update snapshot tests * [Resistor Duo analyzer] Fix linting errors * [Resistor Duo solution] Remove unused variable * [Resistor Duo analyzer] Undo change to TSESTree import * [Resistor Duo analyzer (#127)] Rename helper method * [Resistor Duo analyzer (#127)] Implement requested changes * [Resistor Duo analyzer (exercism#127)] Add exported method signature to the tip --- .../ResistorColorDuoSolution.ts | 22 + .../practice/resistor-color-duo/index.ts | 27 + .../__snapshots__/snapshot.ts.snap | 506 +++++++++++++++--- 3 files changed, 492 insertions(+), 63 deletions(-) diff --git a/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts b/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts index ab2bfc3a..7bc70ab0 100644 --- a/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts +++ b/src/analyzers/practice/resistor-color-duo/ResistorColorDuoSolution.ts @@ -4,6 +4,7 @@ import { ExtractedVariable, extractExports, extractFunctions, + extractVariables, findAll, findFirst, findTopLevelConstants, @@ -480,6 +481,18 @@ class Entry { return parameterName(this.params[0]) } + public get nameOfConstantDefinedInBody(): string | null { + const localConstants = extractVariables(this.body).filter( + (constant) => + constant.init?.type === AST_NODE_TYPES.ArrayExpression || + constant.init?.type === AST_NODE_TYPES.ObjectExpression + ) + if (localConstants.length) { + return localConstants[0].name || 'COLORS' + } + return null + } + public isOptimal( constant: Readonly | undefined, program: Program @@ -508,6 +521,11 @@ class Entry { } } + if (!constant && !!this.nameOfConstantDefinedInBody) { + logger.log('~> found a constant that was not declared at the top level') + return false + } + if (this.hasOneMap) { logger.log('~> is a map solution') return this.isOptimalMapSolution(logger, this.body, constant, program) @@ -1181,6 +1199,10 @@ export class ResistorColorDuoSolution { return this.fileConstants.length === 1 } + public get shouldExtractTopLevelConstant(): boolean { + return !this.mainConstant && !!this.entry.nameOfConstantDefinedInBody + } + public get hasOptimalEntry(): boolean { return this.entry.isOptimal(this.mainConstant, this.program) } diff --git a/src/analyzers/practice/resistor-color-duo/index.ts b/src/analyzers/practice/resistor-color-duo/index.ts index f2356a1b..e077cb91 100644 --- a/src/analyzers/practice/resistor-color-duo/index.ts +++ b/src/analyzers/practice/resistor-color-duo/index.ts @@ -120,6 +120,23 @@ const ISSUE_UNEXPECTED_CALL = factory<'unexpected' | 'expected'>` CommentType.Actionable ) +const PREFER_EXTRACTED_TOP_LEVEL_CONSTANT = factory< + 'value' | 'name' | 'method.signature' +>` +📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const ${'name'} = ${'value'} + +export ${'method.signature'} +\`\`\` +`( + 'javascript.resistor-color-duo.prefer_extracted_top_level_constant', + CommentType.Actionable +) + type Program = TSESTree.Program export class ResistorColorDuoAnalyzer extends IsolatedAnalyzerImpl { @@ -287,6 +304,16 @@ export class ResistorColorDuoAnalyzer extends IsolatedAnalyzerImpl { solution: ResistorColorDuoSolution, output: WritableOutput ): void | never { + if (solution.shouldExtractTopLevelConstant) { + output.add( + PREFER_EXTRACTED_TOP_LEVEL_CONSTANT({ + name: solution.entry.nameOfConstantDefinedInBody, + value: '...', + 'method.signature': solution.entry.signature, + }) + ) + } + if (solution || output) { return } diff --git a/test/analyzers/resistor-color-duo/__snapshots__/snapshot.ts.snap b/test/analyzers/resistor-color-duo/__snapshots__/snapshot.ts.snap index e481a05c..941ec2e9 100644 --- a/test/analyzers/resistor-color-duo/__snapshots__/snapshot.ts.snap +++ b/test/analyzers/resistor-color-duo/__snapshots__/snapshot.ts.snap @@ -86,6 +86,33 @@ lower cognitive complexity.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/10's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const colors = ... + +export function decodedValue(input) ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "function decodedValue(input) ...", + "name": "colors", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more @@ -588,6 +615,33 @@ call\\" with a named call, that can be documented individually.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/105's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const COLORS = ... + +export const decodedValue = (resistorColors) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = (resistorColors) => ...", + "name": "COLORS", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more @@ -1187,26 +1241,62 @@ exports[`When running analysis on resistor-color-duo fixtures and expecting matc IsolatedAnalyzerOutput { "comments": Array [ CommentImpl { - "externalTemplate": "javascript.resistor-color-duo.must_use_a_helper", - "message": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", - "template": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. +\`\`\`javascript +const COLORS = ... -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", +export const decodedValue = colorArray => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = colorArray => ...", + "name": "COLORS", + "value": "...", + }, + }, + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", + "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more +strict than the \`parseXXX\` family and applies in this exercise.", + "template": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more +strict than the \`parseXXX\` family and applies in this exercise.", "type": "actionable", "variables": Object {}, }, + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.limit_number_of_colors", + "message": "💬 Limit the number of input colors that are processed. If more than two colors +are passed in, only the first two colors should be used to calculate the total +\`colorCode\` value. + +📕 (At least) one test case inputs three colors instead of two. If the student +has not accounted for this, they might need to update their solution. Help them +find the button to update. The tests won't pass without limiting the number of +colors.", + "template": "💬 Limit the number of input colors that are processed. If more than two colors +are passed in, only the first two colors should be used to calculate the total +\`colorCode\` value. + +📕 (At least) one test case inputs three colors instead of two. If the student +has not accounted for this, they might need to update their solution. Help them +find the button to update. The tests won't pass without limiting the number of +colors.", + "type": "essential", + "variables": Object {}, + }, ], "summary": undefined, } @@ -1274,6 +1364,33 @@ The tests won't pass without it.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/143's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const CODES = ... + +export const decodedValue = (colors) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = (colors) => ...", + "name": "CODES", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.use_array_comprehensions", "message": "💬 Replace \`.forEach(...)\` with a comprehension such as \`map\`.", @@ -1820,6 +1937,33 @@ strict than the \`parseXXX\` family and applies in this exercise.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/177's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const COLORS = ... + +export const decodedValue = (args) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = (args) => ...", + "name": "COLORS", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more @@ -1872,34 +2016,66 @@ exports[`When running analysis on resistor-color-duo fixtures and expecting matc IsolatedAnalyzerOutput { "comments": Array [ CommentImpl { - "externalTemplate": "javascript.resistor-color-duo.must_use_a_helper", - "message": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", - "template": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. +\`\`\`javascript +const colorMap = ... -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", +export function decodedValue([first, second]) ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", "type": "actionable", - "variables": Object {}, + "variables": Object { + "method.signature": "function decodedValue([first, second]) ...", + "name": "colorMap", + "value": "...", + }, }, ], - "summary": undefined, } `; exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/183's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const colors = ... + +export const decodedValue = ([colorOne, colorTwo]) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = ([colorOne, colorTwo]) => ...", + "name": "colors", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more @@ -1981,24 +2157,60 @@ exports[`When running analysis on resistor-color-duo fixtures and expecting matc IsolatedAnalyzerOutput { "comments": Array [ CommentImpl { - "externalTemplate": "javascript.resistor-color-duo.must_use_a_helper", - "message": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", - "template": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. +\`\`\`javascript +const bandColors = ... -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", +export function decodedValue (colors) ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", "type": "actionable", + "variables": Object { + "method.signature": "function decodedValue (colors) ...", + "name": "bandColors", + "value": "...", + }, + }, + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", + "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more +strict than the \`parseXXX\` family and applies in this exercise.", + "template": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more +strict than the \`parseXXX\` family and applies in this exercise.", + "type": "actionable", + "variables": Object {}, + }, + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.limit_number_of_colors", + "message": "💬 Limit the number of input colors that are processed. If more than two colors +are passed in, only the first two colors should be used to calculate the total +\`colorCode\` value. + +📕 (At least) one test case inputs three colors instead of two. If the student +has not accounted for this, they might need to update their solution. Help them +find the button to update. The tests won't pass without limiting the number of +colors.", + "template": "💬 Limit the number of input colors that are processed. If more than two colors +are passed in, only the first two colors should be used to calculate the total +\`colorCode\` value. + +📕 (At least) one test case inputs three colors instead of two. If the student +has not accounted for this, they might need to update their solution. Help them +find the button to update. The tests won't pass without limiting the number of +colors.", + "type": "essential", "variables": Object {}, }, ], @@ -2262,6 +2474,33 @@ call\\" with a named call, that can be documented individually.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/203's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const COLORS = ... + +export function decodedValue([color1, color2]) ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "function decodedValue([color1, color2]) ...", + "name": "COLORS", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more @@ -2296,6 +2535,33 @@ strict than the \`parseXXX\` family and applies in this exercise.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/205's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const COLORS = ... + +export const decodedValue = (arr) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = (arr) => ...", + "name": "COLORS", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.use_array_comprehensions", "message": "💬 Replace \`for(...) { }\` with a comprehension such as \`map\`.", @@ -2544,6 +2810,33 @@ call\\" with a named call, that can be documented individually.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/224's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const COLORS = ... + +export const decodedValue = colors => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = colors => ...", + "name": "COLORS", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more @@ -2731,6 +3024,33 @@ IsolatedAnalyzerOutput { exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/235's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const colors = ... + +export function decodedValue([color1, color2]) ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "function decodedValue([color1, color2]) ...", + "name": "colors", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more @@ -3143,6 +3463,33 @@ call\\" with a named call, that can be documented individually.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/263's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const resistor = ... + +export const decodedValue = (colorsArr) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = (colorsArr) => ...", + "name": "resistor", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.use_array_comprehensions", "message": "💬 Replace \`.forEach(...)\` with a comprehension such as \`map\`.", @@ -3440,25 +3787,31 @@ exports[`When running analysis on resistor-color-duo fixtures and expecting matc IsolatedAnalyzerOutput { "comments": Array [ CommentImpl { - "externalTemplate": "javascript.resistor-color-duo.must_use_a_helper", - "message": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", - "template": "📕 Mentor the student to add helper function and DRY-up this solution. The -solution to \`resistor-color\` can be used as helper method here. When using an -\`Array\` as colors source, in a years time, will the student recall why it's -the _index_ in that array? When using an \`Object\`, what does the value mean? -Re-using \`colorCode\` explains this in both cases. +\`\`\`javascript +const COLORS = ... -💬 Using a helper method is good practice, because it replaces a cryptic \\"member -call\\" with a named call, that can be documented individually.", +export const decodedValue = (colorArray) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", "type": "actionable", - "variables": Object {}, + "variables": Object { + "method.signature": "const decodedValue = (colorArray) => ...", + "name": "COLORS", + "value": "...", + }, }, ], "summary": undefined, @@ -3651,6 +4004,33 @@ strict than the \`parseXXX\` family and applies in this exercise.", exports[`When running analysis on resistor-color-duo fixtures and expecting matches resistor-color-duo/289's output: output 1`] = ` IsolatedAnalyzerOutput { "comments": Array [ + CommentImpl { + "externalTemplate": "javascript.resistor-color-duo.prefer_extracted_top_level_constant", + "message": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const array = ... + +export const decodedValue = (colors) => ... +\`\`\`", + "template": "📕 Instead of defining the constant _inside_ the function, consider extracting it +to the top-level. Constants, functions, and classes that are not \`export\`ed, +are not accessible from outside the file. + +\`\`\`javascript +const %{name} = %{value} + +export %{method.signature} +\`\`\`", + "type": "actionable", + "variables": Object { + "method.signature": "const decodedValue = (colors) => ...", + "name": "array", + "value": "...", + }, + }, CommentImpl { "externalTemplate": "javascript.resistor-color-duo.prefer_number_over_parse", "message": "💬 Use \`Number(...)\` when the input is expected to be a number. It's more From 07a57b14bc81dfa9b33cb5558133fa222e335b14 Mon Sep 17 00:00:00 2001 From: keiravillekode Date: Tue, 1 Jul 2025 09:03:47 +1000 Subject: [PATCH 02/13] workflows use ubuntu-24.04 (#167) --- .github/workflows/ci.js.yml | 6 +++--- .github/workflows/codeql.yml | 2 +- .github/workflows/format-code.yml | 2 +- .github/workflows/pr.ci.js.yml | 6 +++--- .github/workflows/verify-code-formatting.yml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml index 26e6bb6d..a4cd4faf 100644 --- a/.github/workflows/ci.js.yml +++ b/.github/workflows/ci.js.yml @@ -9,7 +9,7 @@ on: jobs: precheck: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 @@ -25,7 +25,7 @@ jobs: run: yarn lint unit_tests: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: @@ -48,7 +48,7 @@ jobs: run: yarn test:e2e smoke_tests: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a17a5439..e6a3ea30 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,7 +16,7 @@ on: jobs: analyze: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false diff --git a/.github/workflows/format-code.yml b/.github/workflows/format-code.yml index ad673fb4..65edb9c1 100644 --- a/.github/workflows/format-code.yml +++ b/.github/workflows/format-code.yml @@ -7,7 +7,7 @@ on: jobs: format: name: 'Format code' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/format') steps: - name: 'Post acknowledgement that it will format code' diff --git a/.github/workflows/pr.ci.js.yml b/.github/workflows/pr.ci.js.yml index 8bf67a7e..f2d9678f 100644 --- a/.github/workflows/pr.ci.js.yml +++ b/.github/workflows/pr.ci.js.yml @@ -7,7 +7,7 @@ on: pull_request jobs: precheck: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout PR @@ -25,7 +25,7 @@ jobs: run: yarn lint unit_tests: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: @@ -50,7 +50,7 @@ jobs: run: yarn test:e2e smoke_tests: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 diff --git a/.github/workflows/verify-code-formatting.yml b/.github/workflows/verify-code-formatting.yml index c7f686d5..4878b89e 100644 --- a/.github/workflows/verify-code-formatting.yml +++ b/.github/workflows/verify-code-formatting.yml @@ -7,7 +7,7 @@ on: jobs: verify: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: 'Checkout code' uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 From 225bec895f6b9059df8f1e19a970ceb67bdcc111 Mon Sep 17 00:00:00 2001 From: Derk-Jan Karrenbeld Date: Thu, 2 Oct 2025 17:27:39 +0200 Subject: [PATCH 03/13] Update README.md (#169) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35fda93d..d2cbb7b4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # @exercism/javascript-analyzer [![javascript-analyzer / main](https://github.com/exercism/javascript-analyzer/actions/workflows/ci.js.yml/badge.svg)](https://github.com/exercism/javascript-analyzer/actions/workflows/ci.js.yml) -[![javascript-analyzer / deploy](https://github.com/exercism/javascript-analyzer/actions/workflows/deploys.yml/badge.svg)](https://github.com/exercism/javascript-analyzer/actions/workflows/deploys.yml) +[![Deploy](https://github.com/exercism/javascript-analyzer/actions/workflows/deploy.yml/badge.svg)](https://github.com/exercism/javascript-analyzer/actions/workflows/deploy.yml) Runs static analysis using [`@exercism/static-analysis`][git-static-analysis] on exercises from the [`@exercism/javascript` track][git-javascript]. From b88885078fdb7643bbba2e21c4751389a85c8afa Mon Sep 17 00:00:00 2001 From: Samir Husain Date: Tue, 20 Jan 2026 04:53:06 +0530 Subject: [PATCH 04/13] two-fer: detect and block stub throw implementations (#170) * two-fer: detect and block stub throw implementations * two-fer: detect unreachable stub throw after return * two-fer: simplify stub throw detection * Detect and block Exercism stub throw in two-fer analyzer * Format extract_main_method.ts with Prettier --- src/analyzers/practice/two-fer/index.ts | 12 +++++- src/analyzers/utils/extract_main_method.ts | 38 +++++++++++++++++++ src/comments/remove_stub_throw.ts | 5 +++ test/analyzers/two-fer/stub-throw.ts | 43 ++++++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/comments/remove_stub_throw.ts create mode 100644 test/analyzers/two-fer/stub-throw.ts diff --git a/src/analyzers/practice/two-fer/index.ts b/src/analyzers/practice/two-fer/index.ts index 6203d186..5b49a5bb 100644 --- a/src/analyzers/practice/two-fer/index.ts +++ b/src/analyzers/practice/two-fer/index.ts @@ -36,6 +36,8 @@ import { import { extractNamedFunction } from '~src/extracts/extract_named_function' import { makeNoSourceOutput } from '~src/output/makeNoSourceOutput' import { makeParseErrorOutput } from '~src/output/makeParseErrorOutput' +import { hasStubThrow } from '~src/analyzers/utils/extract_main_method' +import { REMOVE_STUB_THROW } from '~src/comments/remove_stub_throw' type ConditionalExpression = TSESTree.ConditionalExpression type IfStatement = TSESTree.IfStatement @@ -82,7 +84,7 @@ export class TwoFerAnalyzer extends AnalyzerImpl { private program!: Program private source!: string - private mainMethod!: ExtractedFunction + private mainMethod?: ExtractedFunction protected async execute(input: Input): Promise { const [parsed] = await this.parse(input) @@ -90,11 +92,17 @@ export class TwoFerAnalyzer extends AnalyzerImpl { this.program = parsed.program this.source = parsed.source - this.mainMethod = extractNamedFunction('twoFer', this.program)! + this.mainMethod = extractNamedFunction('twoFer', this.program) // Firstly we want to check that the structure of this solution is correct // and that there is nothing structural stopping it from passing the tests this.checkStructure() + if (!this.mainMethod) return + + if (hasStubThrow(this.mainMethod)) { + this.disapprove(REMOVE_STUB_THROW()) + return + } // Now we want to ensure that the method signature is sane and that it has // valid arguments diff --git a/src/analyzers/utils/extract_main_method.ts b/src/analyzers/utils/extract_main_method.ts index 6232d55b..a49ad33c 100644 --- a/src/analyzers/utils/extract_main_method.ts +++ b/src/analyzers/utils/extract_main_method.ts @@ -79,3 +79,41 @@ export function extractMainMethod( return undefined } + +function isNewExpression(node: unknown): node is TSESTree.NewExpression { + return ( + typeof node === 'object' && + node !== null && + (node as TSESTree.Node).type === 'NewExpression' + ) +} + +function isStubThrowStatement(statement: TSESTree.Statement): boolean { + if (statement.type !== 'ThrowStatement') return false + + const argument = statement.argument + if (!isNewExpression(argument)) return false + + const callee = argument.callee + if (callee.type !== 'Identifier' || callee.name !== 'Error') return false + + const [firstArg] = argument.arguments + if (!firstArg || firstArg.type !== 'Literal') return false + if (typeof firstArg.value !== 'string') return false + + return ( + firstArg.value.includes('Please implement') || + firstArg.value.includes('Remove this line and implement') || + firstArg.value.includes('Implement the') || + firstArg.value.includes('Remove this statement and implement') + ) +} + +export function hasStubThrow(fn: { body?: TSESTree.Node }): boolean { + if (!fn.body || fn.body.type !== 'BlockStatement') return false + + return fn.body.body.some( + (statement) => + statement.type === 'ThrowStatement' && isStubThrowStatement(statement) + ) +} diff --git a/src/comments/remove_stub_throw.ts b/src/comments/remove_stub_throw.ts new file mode 100644 index 00000000..a0f4eae4 --- /dev/null +++ b/src/comments/remove_stub_throw.ts @@ -0,0 +1,5 @@ +import { factory, CommentType } from '~src/comments/comment' + +export const REMOVE_STUB_THROW = factory` +Remove this placeholder throw statement. It is dead code. +`('javascript.general.remove_stub_throw', CommentType.Essential) diff --git a/test/analyzers/two-fer/stub-throw.ts b/test/analyzers/two-fer/stub-throw.ts new file mode 100644 index 00000000..01472718 --- /dev/null +++ b/test/analyzers/two-fer/stub-throw.ts @@ -0,0 +1,43 @@ +import { TwoFerAnalyzer } from '~src/analyzers/practice/two-fer' +import { makeAnalyze } from '~test/helpers/smoke' + +const analyze = makeAnalyze(() => new TwoFerAnalyzer()) + +describe('two-fer stub throw detection', () => { + it('blocks canonical exercism stub throw', async () => { + const solution = ` + export function twoFer(name = 'you') { + return 'One for you, one for me.' + throw new Error('Remove this line and implement the function'); + } + `.trim() + + const output = await analyze(solution) + + const stub = output.comments.find( + (c) => c.externalTemplate === 'javascript.general.remove_stub_throw' + ) + + expect(stub).toBeDefined() + expect(stub?.type).toBe('essential') + }) + + it('does not block normal error throws', async () => { + const solution = ` + export function twoFer(name = 'you') { + if (name === 'bad') { + throw new Error('Invalid name'); + } + return \`One for \${name}, one for me.\` + } + `.trim() + + const output = await analyze(solution) + + const stub = output.comments.find( + (c) => c.externalTemplate === 'javascript.general.remove_stub_throw' + ) + + expect(stub).toBeUndefined() + }) +}) From a7b79d7a4d7e3aca91b3a66e315250b47c4e6d6d Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Tue, 2 Jun 2026 11:45:30 -0700 Subject: [PATCH 05/13] Use a pinned hash for the base Docker image (#177) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d25766be..28b6c5d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts-alpine as builder +FROM node:lts-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS builder # Install SSL ca certificates RUN apk update && apk add ca-certificates @@ -17,7 +17,7 @@ RUN yarn install RUN yarn install --production --modules-folder './production_node_modules' # Build a minimal and secured container -FROM node:lts-alpine +FROM node:lts-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /javascript-analyzer/package.json /opt/analyzer/package.json From 11753c4429e5e65823b381fd2b2c17d572ff0005 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:08:29 +0200 Subject: [PATCH 06/13] Bump picomatch from 2.2.2 to 2.3.2 (#176) Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.2.2 to 2.3.2. - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/picomatch/compare/2.2.2...2.3.2) --- updated-dependencies: - dependency-name: picomatch dependency-version: 2.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 60d4823e..14ca62c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4664,15 +4664,10 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.3.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" + integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== pify@^4.0.1: version "4.0.1" From b3fd065b6ae71a93b98ad7c214bb84554440c8a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:08:44 +0200 Subject: [PATCH 07/13] Bump lodash from 4.17.21 to 4.18.1 (#175) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.18.1. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.1) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.18.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 14ca62c8..571ae8f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4305,9 +4305,9 @@ lodash.sortby@^4.7.0: integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash@^4.17.13, lodash@^4.17.19, lodash@^4.7.0: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== lru-cache@^6.0.0: version "6.0.0" From 13126e6891ed64dcdc90cfd3e6d3c3f728577334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:08:51 +0200 Subject: [PATCH 08/13] Bump flatted from 3.1.0 to 3.4.2 (#174) Bumps [flatted](https://github.com/WebReflection/flatted) from 3.1.0 to 3.4.2. - [Commits](https://github.com/WebReflection/flatted/compare/v3.1.0...v3.4.2) --- updated-dependencies: - dependency-name: flatted dependency-version: 3.4.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 571ae8f6..cacc2ca3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3137,9 +3137,9 @@ flat-cache@^3.0.4: rimraf "^3.0.2" flatted@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067" - integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA== + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== form-data@^3.0.0: version "3.0.1" From b7d9a071fac76263bdc15a309c5ac82af7be11c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:09:03 +0200 Subject: [PATCH 09/13] Bump minimatch from 3.1.2 to 3.1.5 (#173) Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5. - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5) --- updated-dependencies: - dependency-name: minimatch dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cacc2ca3..03089b3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4374,9 +4374,9 @@ mimic-fn@^2.1.0: integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== dependencies: brace-expansion "^1.1.7" From 93ca364f3bbf2343b81b8838600db9a22914dedc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:09:21 +0200 Subject: [PATCH 10/13] Bump form-data from 3.0.1 to 3.0.5 (#168) Bumps [form-data](https://github.com/form-data/form-data) from 3.0.1 to 3.0.5. - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v3.0.1...v3.0.5) --- updated-dependencies: - dependency-name: form-data dependency-version: 3.0.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 130 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 117 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 03089b3d..4fd13332 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2372,6 +2372,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce" @@ -2707,6 +2715,15 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + electron-to-chromium@^1.3.811: version "1.3.830" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.830.tgz#40e3144204f8ca11b2cebec83cf14c20d3499236" @@ -2781,6 +2798,33 @@ es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19 string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" + integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -3142,13 +3186,15 @@ flatted@^3.1.0: integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + version "3.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.5.tgz#2ea3ec24f0dcb7e0262a11efb732031240ea8e9f" + integrity sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" - mime-types "^2.1.12" + es-set-tostringtag "^2.1.0" + hasown "^2.0.4" + mime-types "^2.1.35" fs-readdir-recursive@^1.1.0: version "1.1.0" @@ -3170,6 +3216,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -3218,6 +3269,30 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.2.6: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -3293,6 +3368,11 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -3345,6 +3425,11 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -3352,6 +3437,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -3359,6 +3451,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.2, hasown@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -4338,6 +4437,11 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4356,17 +4460,17 @@ micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" -mime-db@1.42.0: - version "1.42.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" - integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12: - version "2.1.25" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" - integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== +mime-types@^2.1.35: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - mime-db "1.42.0" + mime-db "1.52.0" mimic-fn@^2.1.0: version "2.1.0" From b9e0ad4dbee3fc4334b9ad559d22987c4c7f5611 Mon Sep 17 00:00:00 2001 From: Isaac Good Date: Thu, 23 Jul 2026 17:38:46 -0700 Subject: [PATCH 11/13] Update GHA workflow versions: Node LTS=24, active=22,24; Ubuntu 26.04 (#182) * Update GHA workflow versions: Node LTS=24, active=22,24; Ubuntu 26.04 * Run tests on Node 22.x as well as 24 and 26 --- .github/workflows/ci.js.yml | 12 ++++++------ .github/workflows/pr.ci.js.yml | 14 +++++++------- Dockerfile | 5 +++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml index a4cd4faf..765e5419 100644 --- a/.github/workflows/ci.js.yml +++ b/.github/workflows/ci.js.yml @@ -9,14 +9,14 @@ on: jobs: precheck: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 - - name: Use Node.js LTS (16.x) + - name: Use Node.js LTS (24.x) uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 24.x - name: Install project dependencies run: yarn install --frozen-lockfile --ignore-scripts @@ -25,11 +25,11 @@ jobs: run: yarn lint unit_tests: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 strategy: matrix: - node-version: [14.x, 16.x] + node-version: [22.x, 24.x, 26.x] steps: - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 @@ -48,7 +48,7 @@ jobs: run: yarn test:e2e smoke_tests: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: Checkout code uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 diff --git a/.github/workflows/pr.ci.js.yml b/.github/workflows/pr.ci.js.yml index f2d9678f..9616546d 100644 --- a/.github/workflows/pr.ci.js.yml +++ b/.github/workflows/pr.ci.js.yml @@ -7,16 +7,16 @@ on: pull_request jobs: precheck: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: Checkout PR uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 - - name: Use Node.js LTS (16.x) + - name: Use Node.js LTS (24.x) uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 24.x - name: Install project dependencies run: yarn install --frozen-lockfile --ignore-scripts @@ -24,12 +24,12 @@ jobs: - name: Run exercism/javascript ci precheck (lint code) run: yarn lint - unit_tests: - runs-on: ubuntu-24.04 + ci: + runs-on: ubuntu-26.04 strategy: matrix: - node-version: [14.x, 16.x] + node-version: [22.x, 24.x, 26.x] steps: - name: Checkout PR @@ -50,7 +50,7 @@ jobs: run: yarn test:e2e smoke_tests: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: Checkout code uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 diff --git a/Dockerfile b/Dockerfile index 28b6c5d3..9b0cf973 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM node:lts-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS builder +# sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd => node:24.18.0-alpine3.24 +FROM node:lts-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS builder # Install SSL ca certificates RUN apk update && apk add ca-certificates @@ -17,7 +18,7 @@ RUN yarn install RUN yarn install --production --modules-folder './production_node_modules' # Build a minimal and secured container -FROM node:lts-alpine3.23@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f +FROM node:lts-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/passwd /etc/passwd COPY --from=builder /javascript-analyzer/package.json /opt/analyzer/package.json From 732a6f4a8fb90f6370f2fcad3b6361a87a69bac1 Mon Sep 17 00:00:00 2001 From: Derk-Jan Karrenbeld Date: Mon, 7 Sep 2026 09:50:30 +0200 Subject: [PATCH 12/13] Update ci.js.yml (#185) --- .github/workflows/ci.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.js.yml b/.github/workflows/ci.js.yml index 765e5419..6ae6c8cb 100644 --- a/.github/workflows/ci.js.yml +++ b/.github/workflows/ci.js.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: - node-version: [22.x, 24.x, 26.x] + node-version: [24.x, 26.x] steps: - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 From 89372a9d21b90e96a4b670a913677ca82ff71b9c Mon Sep 17 00:00:00 2001 From: Derk-Jan Karrenbeld Date: Mon, 7 Sep 2026 09:58:11 +0200 Subject: [PATCH 13/13] Update pr.ci.js.yml (#186) --- .github/workflows/pr.ci.js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.ci.js.yml b/.github/workflows/pr.ci.js.yml index 9616546d..2ea0ac94 100644 --- a/.github/workflows/pr.ci.js.yml +++ b/.github/workflows/pr.ci.js.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: - node-version: [22.x, 24.x, 26.x] + node-version: [24.x, 26.x] steps: - name: Checkout PR