diff --git a/.gitignore b/.gitignore index 3f1f810..6b02fdc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ elm-stuff bin/*.js src/main.js *analysis.json +*tags.json node_modules diff --git a/bin/check_files.sh b/bin/check_files.sh index 2be02c3..cffdae3 100755 --- a/bin/check_files.sh +++ b/bin/check_files.sh @@ -37,13 +37,30 @@ function main { exit 1 fi - jq -S . ${exercise}/expected_analysis.json > /tmp/expected.json - jq -S . ${exercise}/analysis.json > /tmp/actual.json + jq -S '.comments |= sort' ${exercise}/expected_analysis.json > /tmp/expected.json + jq -S '.comments |= sort' ${exercise}/analysis.json > /tmp/actual.json if ! diff /tmp/expected.json /tmp/actual.json ;then echo "🔥 ${exercise}: expected ${exercise}/analysis.json to equal ${exercise}/expected_analysis.json on successful run 🔥" exit 1 fi + if [[ ! -f "${exercise}/expected_tags.json" ]]; then + echo "🔥 ${exercise}: expected expected_tags.json to exist 🔥" + exit 1 + fi + + if [[ ! -f "${exercise}/tags.json" ]]; then + echo "🔥 ${exercise}: expected tags.json to exist on successful run 🔥" + exit 1 + fi + + jq -S 'sort' ${exercise}/expected_tags.json > /tmp/expected.json + jq -S 'sort' ${exercise}/tags.json > /tmp/actual.json + if ! diff /tmp/expected.json /tmp/actual.json ;then + echo "🔥 ${exercise}: expected ${exercise}/tags.json to equal ${exercise}/expected_tags.json on successful run 🔥" + exit 1 + fi + echo "🏁 ${exercise}: expected files present and correct after successful run 🏁" } diff --git a/bin/run.sh b/bin/run.sh index 13fc7b6..f192d06 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -10,6 +10,7 @@ set -o pipefail # Catch failures in pipes. # Remove trailing slash for elm-review INPUT_DIR=${2%/} OUTPUT_DIR=${3%/} +OUTPUT_TAGS=${4:-false} # Check if script running in docker if [ -f solution_cache.tar ]; then @@ -20,14 +21,24 @@ if [ -f solution_cache.tar ]; then INPUT_DIR=/tmp/sol fi -# Run analysis # Temporarily disable -e mode set +e + +# Run analysis npx elm-review $INPUT_DIR \ --elmjson $INPUT_DIR/elm.json \ --config . \ --report=json \ - | node ./bin/cli.js > $OUTPUT_DIR/analysis.json + --extract \ + > /tmp/elm-review-report.json set -e +# Output comments +cat /tmp/elm-review-report.json | node ./bin/cli.js > $OUTPUT_DIR/analysis.json + +if [ $OUTPUT_TAGS = "--tags" ]; then + # Output tags + jq '.extracts | to_entries | map(.value) | add | sort' /tmp/elm-review-report.json > $OUTPUT_DIR/tags.json +fi + echo Finished diff --git a/bin/smoke_test.sh b/bin/smoke_test.sh index 8b1748c..720eba9 100755 --- a/bin/smoke_test.sh +++ b/bin/smoke_test.sh @@ -8,7 +8,7 @@ set -o pipefail # Catch failures in pipes. for solution in test_data/*/* ; do slug=$(basename $(dirname $solution)) # run analysis - bin/run.sh $slug $solution $solution + bin/run.sh $slug $solution $solution --tags > /dev/null # check result bin/check_files.sh $solution done diff --git a/src/ElmSyntaxHelpers.elm b/src/ElmSyntaxHelpers.elm index ca21018..7fc5fcb 100644 --- a/src/ElmSyntaxHelpers.elm +++ b/src/ElmSyntaxHelpers.elm @@ -1,5 +1,6 @@ -module ElmSyntaxHelpers exposing (hasGenericRecord, traversePattern, typeAnnotationsMatch) +module ElmSyntaxHelpers exposing (hasDestructuringPattern, hasGenericRecord, hasTyped, traversePattern, typeAnnotationsMatch) +import Elm.Syntax.ModuleName exposing (ModuleName) import Elm.Syntax.Node as Node exposing (Node(..)) import Elm.Syntax.Pattern exposing (Pattern(..)) import Elm.Syntax.TypeAnnotation exposing (TypeAnnotation(..)) @@ -76,6 +77,61 @@ hasGenericRecord annotation = False +hasTyped : ModuleName -> String -> Node TypeAnnotation -> Bool +hasTyped moduleName name annotation = + case Node.value annotation of + Typed (Node _ ( typeModule, typeName )) annotations -> + (typeModule == moduleName && typeName == name) + || List.any (hasTyped moduleName name) annotations + + Record recordFields -> + recordFields + |> List.map (Node.value >> Tuple.second) + |> List.any (hasTyped moduleName name) + + GenericRecord _ (Node _ recordFields) -> + recordFields + |> List.map (Node.value >> Tuple.second) + |> List.any (hasTyped moduleName name) + + Tupled annotations -> + List.any (hasTyped moduleName name) annotations + + FunctionTypeAnnotation a b -> + hasTyped moduleName name a || hasTyped moduleName name b + + GenericType _ -> + False + + Unit -> + False + + +hasDestructuringPattern : Node Pattern -> Bool +hasDestructuringPattern fullPattern = + let + destructuringPattern pattern = + case Node.value pattern of + RecordPattern _ -> + True + + UnConsPattern _ _ -> + True + + TuplePattern _ -> + True + + NamedPattern _ _ -> + True + + _ -> + False + in + fullPattern + |> traversePattern + |> List.any destructuringPattern + + traversePattern : Node Pattern -> List (Node Pattern) traversePattern pattern = case Node.value pattern of diff --git a/src/ReviewConfig.elm b/src/ReviewConfig.elm index c02fda6..c8672dd 100644 --- a/src/ReviewConfig.elm +++ b/src/ReviewConfig.elm @@ -25,12 +25,16 @@ import Exercise.ValentinesDay import Exercise.ZebraPuzzle import Review.Rule as Rule exposing (Rule) import RuleConfig exposing (RuleConfig) +import Tags ruleConfigs : List RuleConfig ruleConfigs = - [ -- Common Rules - Common.NoUnused.ruleConfig + [ -- Tags + Tags.ruleConfig + + -- Common Rules + , Common.NoUnused.ruleConfig , Common.Simplify.ruleConfig , Common.NoDebug.ruleConfig , Common.UseCamelCase.ruleConfig diff --git a/src/RuleConfig.elm b/src/RuleConfig.elm index dc26351..eb7e720 100644 --- a/src/RuleConfig.elm +++ b/src/RuleConfig.elm @@ -14,6 +14,7 @@ type alias RuleConfig = type AnalyzerRule = CustomRule (Comment -> Rule) Comment | ImportedRule Rule (Comment -> Decoder Comment) Comment + | TagRule Rule analyzerRuleToRule : AnalyzerRule -> Rule @@ -25,6 +26,9 @@ analyzerRuleToRule analyzerRule = ImportedRule rule _ _ -> rule + TagRule rule -> + rule + analyzerRuleToDecoder : AnalyzerRule -> Maybe (Decoder Comment) analyzerRuleToDecoder analyzerRule = @@ -35,15 +39,21 @@ analyzerRuleToDecoder analyzerRule = ImportedRule _ toDecoder comment -> Just (toDecoder comment) + TagRule _ -> + Nothing + -analyzerRuleToComment : AnalyzerRule -> Comment +analyzerRuleToComment : AnalyzerRule -> Maybe Comment analyzerRuleToComment analyzerRule = case analyzerRule of CustomRule _ comment -> - comment + Just comment ImportedRule _ _ comment -> - comment + Just comment + + TagRule _ -> + Nothing getRules : RuleConfig -> List Rule @@ -63,7 +73,7 @@ getDecoders = getComments : List RuleConfig -> List Comment getComments = - List.concatMap (.rules >> List.map analyzerRuleToComment) + List.concatMap (.rules >> List.filterMap analyzerRuleToComment) makeConfig : List RuleConfig -> List Rule diff --git a/src/Tags.elm b/src/Tags.elm new file mode 100644 index 0000000..3fe6f2e --- /dev/null +++ b/src/Tags.elm @@ -0,0 +1,480 @@ +module Tags exposing (commonTagsRule, expressionTagsRule, ruleConfig) + +import Elm.Syntax.Declaration exposing (Declaration(..)) +import Elm.Syntax.Expression exposing (Expression(..), FunctionImplementation, LetDeclaration(..)) +import Elm.Syntax.Module exposing (Module) +import Elm.Syntax.Node as Node exposing (Node(..)) +import Elm.Syntax.Type exposing (Type) +import ElmSyntaxHelpers +import Json.Encode as Encode exposing (Value) +import Review.ModuleNameLookupTable as LookupTable exposing (ModuleNameLookupTable) +import Review.Rule as Rule exposing (Rule) +import RuleConfig exposing (AnalyzerRule(..), RuleConfig) +import Set exposing (Set) + + +type alias ProjectContext = + { tags : Set String + } + + +type alias ModuleContext = + { lookupTable : ModuleNameLookupTable + , tags : Set String + } + + +ruleConfig : RuleConfig +ruleConfig = + { restrictToFiles = Nothing + , rules = + [ TagRule commonTagsRule + , TagRule expressionTagsRule + ] + } + + +commonTagsRule : Rule +commonTagsRule = + Rule.newProjectRuleSchema "commonTags" emptyProjectContext + |> Rule.withModuleVisitor (Rule.withModuleDefinitionVisitor commonModuleVisitor) + |> Rule.withModuleContextUsingContextCreator + { fromModuleToProject = fromModuleToProject + , fromProjectToModule = fromProjectToModule + , foldProjectContexts = foldProjectContexts + } + |> Rule.withDataExtractor dataExtractor + |> Rule.fromProjectRuleSchema + + +expressionTagsRule : Rule +expressionTagsRule = + Rule.newProjectRuleSchema "expressionTags" emptyProjectContext + |> Rule.withModuleVisitor + (Rule.withDeclarationEnterVisitor declarationVisitor + >> Rule.withExpressionEnterVisitor expressionVisitor + >> Rule.withModuleDocumentationVisitor documentationVisitor + >> Rule.withCommentsVisitor commentsVisitor + ) + |> Rule.withModuleContextUsingContextCreator + { fromModuleToProject = fromModuleToProject + , fromProjectToModule = fromProjectToModule + , foldProjectContexts = foldProjectContexts + } + |> Rule.withDataExtractor dataExtractor + |> Rule.fromProjectRuleSchema + + +emptyProjectContext : ProjectContext +emptyProjectContext = + ProjectContext Set.empty + + +commonModuleVisitor : Node Module -> ModuleContext -> ( List never, ModuleContext ) +commonModuleVisitor _ context = + ( [], { context | tags = commonTags } ) + + +commonTags : Set String +commonTags = + Set.fromList + [ "paradigm:functional" + , "technique:immutability" + , "uses:module" + ] + + +fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext +fromProjectToModule = + Rule.initContextCreator + (\lookupTable _ -> + { lookupTable = lookupTable + , tags = Set.empty + } + ) + |> Rule.withModuleNameLookupTable + + +fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext +fromModuleToProject = + Rule.initContextCreator + (\isFileIgnored { tags } -> + if isFileIgnored then + emptyProjectContext + + else + ProjectContext tags + ) + |> Rule.withIsFileIgnored + + +foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext +foldProjectContexts a b = + ProjectContext (Set.union a.tags b.tags) + + +dataExtractor : ProjectContext -> Value +dataExtractor = + .tags >> Set.toList >> Encode.list Encode.string + + +declarationVisitor : Node Declaration -> ModuleContext -> ( List never, ModuleContext ) +declarationVisitor node ({ tags } as context) = + case Node.value node of + FunctionDeclaration { documentation, declaration } -> + let + ( _, docContext ) = + documentationVisitor documentation context + + argTags = + functionImplementationTags declaration + in + ( [], { context | tags = Set.union tags (Set.union docContext.tags argTags) } ) + + AliasDeclaration _ -> + ( [], { context | tags = Set.insert "uses:type-alias" tags } ) + + CustomTypeDeclaration customType -> + ( [], { context | tags = Set.union (analyzeCustomType customType) tags } ) + + _ -> + ( [], context ) + + +analyzeCustomType : Type -> Set String +analyzeCustomType { name, generics, constructors } = + let + unionTags = + case constructors of + _ :: _ :: _ -> + [ "uses:union-type" ] + + _ -> + [] + + genericsTags = + case generics of + [] -> + [] + + _ -> + [ "construct:generic-type" ] + + isRecursive = + constructors + |> List.concatMap (Node.value >> .arguments) + |> List.any (ElmSyntaxHelpers.hasTyped [] (Node.value name)) + + recursiveTags = + if isRecursive then + [ "construct:recursive-type" ] + + else + [] + in + Set.fromList ("uses:custom-type" :: genericsTags ++ unionTags ++ recursiveTags) + + +documentationVisitor : Maybe documentation -> ModuleContext -> ( List never, ModuleContext ) +documentationVisitor maybeDoc ({ tags } as context) = + let + docTags = + Set.fromList [ "construct:comment", "construct:documentation" ] + in + case maybeDoc of + Nothing -> + ( [], context ) + + Just _ -> + ( [], { context | tags = Set.union docTags tags } ) + + +commentsVisitor : List (Node String) -> ModuleContext -> ( List never, ModuleContext ) +commentsVisitor comments ({ tags } as context) = + case comments of + [] -> + ( [], context ) + + _ -> + ( [], { context | tags = Set.insert "construct:comment" tags } ) + + +functionImplementationTags : Node FunctionImplementation -> Set String +functionImplementationTags (Node _ { arguments }) = + if List.any ElmSyntaxHelpers.hasDestructuringPattern arguments then + Set.fromList [ "construct:destructuring", "construct:pattern-matching" ] + + else + Set.empty + + +expressionVisitor : Node Expression -> ModuleContext -> ( List never, ModuleContext ) +expressionVisitor ((Node range expression) as node) ({ lookupTable, tags } as context) = + let + matches n = + Set.union (matchExpressionType n) (matchExpression n) + in + case expression of + FunctionOrValue _ name -> + case LookupTable.moduleNameFor lookupTable node of + Nothing -> + ( [], { context | tags = Set.union tags (matches node) } ) + + Just originalModuleName -> + ( [], { context | tags = Set.union tags (matches (Node range (FunctionOrValue originalModuleName name))) } ) + + _ -> + ( [], { context | tags = Set.union tags (matches node) } ) + + +{-| Only looks at the type of the expression, not the content +-} +matchExpressionType : Node Expression -> Set String +matchExpressionType (Node range expression) = + case expression of + Application _ -> + Set.singleton "uses:function-application" + + OperatorApplication _ _ _ _ -> + Set.singleton "uses:function-application" + + PrefixOperator _ -> + Set.singleton "uses:prefix-operator" + + UnitExpr -> + Set.singleton "uses:unit" + + Floatable _ -> + Set.fromList [ "construct:float", "construct:floating-point-number" ] + + Integer _ -> + Set.fromList [ "construct:integral-number", "construct:int" ] + + Hex _ -> + Set.fromList [ "construct:hexadecimal-number", "construct:integral-number", "construct:int" ] + + Negation _ -> + Set.singleton "construct:unary-minus" + + Literal _ -> + if range.end.row > range.start.row then + Set.fromList [ "construct:string", "construct:multiline-string" ] + + else + Set.singleton "construct:string" + + LambdaExpression _ -> + Set.singleton "construct:lambda" + + IfBlock _ _ _ -> + Set.fromList [ "construct:if", "construct:boolean" ] + + LetExpression _ -> + Set.singleton "construct:assignment" + + CharLiteral _ -> + Set.singleton "construct:char" + + TupledExpression _ -> + Set.singleton "construct:tuple" + + CaseExpression _ -> + Set.singleton "construct:pattern-matching" + + RecordExpr _ -> + Set.singleton "construct:record" + + RecordAccess _ _ -> + Set.fromList [ "construct:record", "uses:record-access" ] + + RecordAccessFunction _ -> + Set.fromList [ "construct:record", "uses:record-access", "uses:record-access-function" ] + + RecordUpdateExpression _ _ -> + Set.fromList [ "construct:record", "uses:record-update" ] + + ListExpr _ -> + Set.singleton "construct:list" + + GLSLExpression _ -> + Set.singleton "uses:glsl" + + FunctionOrValue _ value -> + case String.uncons value of + Nothing -> + Set.empty + + Just ( first, _ ) -> + if Char.isUpper first then + Set.singleton "construct:constructor" + + else + Set.empty + + ParenthesizedExpression _ -> + Set.empty + + -- not possible to get in practice + Operator _ -> + Set.empty + + +matchExpression : Node Expression -> Set String +matchExpression (Node _ expression) = + case expression of + FunctionOrValue [ "Bitwise" ] "and" -> + Set.fromList [ "construct:bit-manipulation", "construct:bitwise-and" ] + + FunctionOrValue [ "Bitwise" ] "or" -> + Set.fromList [ "construct:bit-manipulation", "construct:bitwise-or" ] + + FunctionOrValue [ "Bitwise" ] "xor" -> + Set.fromList [ "construct:bit-manipulation", "construct:bitwise-xor" ] + + FunctionOrValue [ "Bitwise" ] "complement" -> + Set.fromList [ "construct:bit-manipulation", "construct:bitwise-not" ] + + FunctionOrValue [ "Bitwise" ] "shiftLeftBy" -> + Set.fromList [ "construct:bit-manipulation", "technique:bit-shifting", "construct:bitwise-left-shift" ] + + FunctionOrValue [ "Bitwise" ] "shiftRightBy" -> + Set.fromList [ "construct:bit-manipulation", "technique:bit-shifting", "construct:bitwise-right-shift" ] + + FunctionOrValue [ "Bitwise" ] "shiftRightZfBy" -> + Set.fromList [ "construct:bit-manipulation", "technique:bit-shifting" ] + + FunctionOrValue [ "Array" ] _ -> + Set.fromList [ "construct:array", "technique:immutable-collection" ] + + FunctionOrValue ("Bytes" :: _) _ -> + Set.singleton "construct:byte" + + FunctionOrValue [ "Set" ] _ -> + Set.fromList [ "construct:set", "technique:immutable-collection", "technique:sorted-collection" ] + + FunctionOrValue [ "Time" ] _ -> + Set.singleton "construct:date-time" + + FunctionOrValue [ "Dict" ] _ -> + Set.fromList [ "construct:dictionary", "technique:immutable-collection", "technique:sorted-collection" ] + + FunctionOrValue [ "List" ] _ -> + Set.singleton "construct:list" + + FunctionOrValue [ "Random" ] _ -> + Set.singleton "technique:randomness" + + FunctionOrValue [ "Regex" ] _ -> + Set.singleton "technique:regular-expression" + + FunctionOrValue [ "Debug" ] _ -> + Set.singleton "uses:debug" + + PrefixOperator "&&" -> + Set.fromList [ "construct:boolean", "construct:logical-and", "technique:boolean-logic" ] + + OperatorApplication "&&" _ _ _ -> + Set.fromList [ "construct:boolean", "construct:logical-and", "technique:boolean-logic" ] + + PrefixOperator "||" -> + Set.fromList [ "construct:boolean", "construct:logical-or", "technique:boolean-logic" ] + + OperatorApplication "||" _ _ _ -> + Set.fromList [ "construct:boolean", "construct:logical-or", "technique:boolean-logic" ] + + FunctionOrValue [ "Basics" ] "not" -> + Set.fromList [ "construct:boolean", "construct:logical-not", "technique:boolean-logic" ] + + FunctionOrValue [ "Basics" ] "xor" -> + Set.fromList [ "construct:boolean", "technique:boolean-logic" ] + + FunctionOrValue [ "Basics" ] "True" -> + Set.singleton "construct:boolean" + + FunctionOrValue [ "Basics" ] "False" -> + Set.singleton "construct:boolean" + + FunctionOrValue [ "Basics" ] "isNaN" -> + Set.fromList [ "construct:boolean", "construct:float", "construct:floating-point-number" ] + + FunctionOrValue [ "Basics" ] "isInfinite" -> + Set.fromList [ "construct:boolean", "construct:float", "construct:floating-point-number" ] + + PrefixOperator "+" -> + Set.singleton "construct:add" + + OperatorApplication "+" _ _ _ -> + Set.singleton "construct:add" + + PrefixOperator "-" -> + Set.singleton "construct:subtract" + + OperatorApplication "-" _ _ _ -> + Set.singleton "construct:subtract" + + PrefixOperator "*" -> + Set.singleton "construct:multiply" + + OperatorApplication "*" _ _ _ -> + Set.singleton "construct:multiply" + + PrefixOperator "/" -> + Set.singleton "construct:divide" + + OperatorApplication "/" _ _ _ -> + Set.singleton "construct:divide" + + PrefixOperator "//" -> + Set.singleton "construct:divide" + + OperatorApplication "//" _ _ _ -> + Set.singleton "construct:divide" + + PrefixOperator "==" -> + Set.fromList [ "construct:equality", "technique:equality-comparison", "construct:boolean" ] + + OperatorApplication "==" _ _ _ -> + Set.fromList [ "construct:equality", "technique:equality-comparison", "construct:boolean" ] + + PrefixOperator "/=" -> + Set.fromList [ "construct:inequality", "technique:equality-comparison", "construct:boolean" ] + + OperatorApplication "/=" _ _ _ -> + Set.fromList [ "construct:inequality", "technique:equality-comparison", "construct:boolean" ] + + LetExpression { declarations } -> + if List.any (Node.value >> letUsesDestructuring) declarations then + Set.fromList [ "construct:destructuring", "construct:pattern-matching" ] + + else + Set.empty + + LambdaExpression { args } -> + if List.any ElmSyntaxHelpers.hasDestructuringPattern args then + Set.fromList [ "construct:destructuring", "construct:pattern-matching" ] + + else + Set.empty + + CaseExpression { cases } -> + if List.any (Tuple.first >> ElmSyntaxHelpers.hasDestructuringPattern) cases then + Set.fromList [ "construct:destructuring", "construct:pattern-matching" ] + + else + Set.empty + + _ -> + Set.empty + + +letUsesDestructuring : LetDeclaration -> Bool +letUsesDestructuring letDeclaration = + case letDeclaration of + LetDestructuring _ _ -> + True + + LetFunction { declaration } -> + declaration + |> Node.value + |> .arguments + |> List.any ElmSyntaxHelpers.hasDestructuringPattern diff --git a/test_data/strain/imperfect_solution/expected_tags.json b/test_data/strain/imperfect_solution/expected_tags.json new file mode 100644 index 0000000..78ab901 --- /dev/null +++ b/test_data/strain/imperfect_solution/expected_tags.json @@ -0,0 +1,11 @@ +[ + "construct:boolean", + "construct:comment", + "construct:list", + "construct:logical-not", + "paradigm:functional", + "technique:boolean-logic", + "technique:immutability", + "uses:function-application", + "uses:module" +] diff --git a/test_data/strain/perfect_solution/expected_tags.json b/test_data/strain/perfect_solution/expected_tags.json new file mode 100644 index 0000000..d4607ac --- /dev/null +++ b/test_data/strain/perfect_solution/expected_tags.json @@ -0,0 +1,12 @@ +[ + "construct:boolean", + "construct:if", + "construct:lambda", + "construct:list", + "construct:logical-not", + "paradigm:functional", + "technique:boolean-logic", + "technique:immutability", + "uses:function-application", + "uses:module" +] diff --git a/test_data/two-fer/imperfect_solution/expected_tags.json b/test_data/two-fer/imperfect_solution/expected_tags.json new file mode 100644 index 0000000..98df01b --- /dev/null +++ b/test_data/two-fer/imperfect_solution/expected_tags.json @@ -0,0 +1,19 @@ +[ + "construct:boolean", + "construct:comment", + "construct:constructor", + "construct:destructuring", + "construct:if", + "construct:int", + "construct:integral-number", + "construct:logical-and", + "construct:multiply", + "construct:pattern-matching", + "construct:string", + "paradigm:functional", + "technique:boolean-logic", + "technique:immutability", + "uses:custom-type", + "uses:function-application", + "uses:module" +] diff --git a/test_data/two-fer/perfect_solution/expected_tags.json b/test_data/two-fer/perfect_solution/expected_tags.json new file mode 100644 index 0000000..55b39b2 --- /dev/null +++ b/test_data/two-fer/perfect_solution/expected_tags.json @@ -0,0 +1,7 @@ +[ + "construct:string", + "paradigm:functional", + "technique:immutability", + "uses:function-application", + "uses:module" +] diff --git a/tests/ElmSyntaxHelpersTest.elm b/tests/ElmSyntaxHelpersTest.elm index 6804c0a..623cc6c 100644 --- a/tests/ElmSyntaxHelpersTest.elm +++ b/tests/ElmSyntaxHelpersTest.elm @@ -50,6 +50,65 @@ hasGenericRecordTests = ] +hasTypedTests : Test +hasTypedTests = + describe "hasTyped" + [ test "no typed" <| + \_ -> + Node.empty + (Tupled + [ Node.empty (FunctionTypeAnnotation (Node.empty Unit) (Node.empty (GenericType "x"))) + , Node.empty (Record [ Node.empty ( Node.empty "x", Node.empty Unit ) ]) + , Node.empty (Typed (Node.empty ( [ "X" ], "y" )) []) + ] + ) + |> ElmSyntaxHelpers.hasTyped [ "X" ] "x" + |> Expect.equal False + , test "one typed" <| + \_ -> + Node.empty + (Tupled + [ Node.empty (FunctionTypeAnnotation (Node.empty Unit) (Node.empty (GenericType "x"))) + , Node.empty (Record [ Node.empty ( Node.empty "x", Node.empty Unit ) ]) + , Node.empty (Typed (Node.empty ( [ "X" ], "x" )) []) + ] + ) + |> ElmSyntaxHelpers.hasTyped [ "X" ] "x" + |> Expect.equal True + ] + + +hasDestructuringPatternTests : Test +hasDestructuringPatternTests = + describe "hasDestructuringPattern" + [ test "no destructuring" <| + \_ -> + Node.empty (ParenthesizedPattern (Node.empty (ListPattern [ Node.empty UnitPattern ]))) + |> ElmSyntaxHelpers.hasDestructuringPattern + |> Expect.equal False + , test "tuple destructuring" <| + \_ -> + Node.empty (TuplePattern []) + |> ElmSyntaxHelpers.hasDestructuringPattern + |> Expect.equal True + , test "record destructuring" <| + \_ -> + Node.empty (RecordPattern []) + |> ElmSyntaxHelpers.hasDestructuringPattern + |> Expect.equal True + , test "named destructuring" <| + \_ -> + Node.empty (NamedPattern { moduleName = [], name = "Thing" } []) + |> ElmSyntaxHelpers.hasDestructuringPattern + |> Expect.equal True + , test "uncons destructuring" <| + \_ -> + Node.empty (UnConsPattern (Node.empty UnitPattern) (Node.empty UnitPattern)) + |> ElmSyntaxHelpers.hasDestructuringPattern + |> Expect.equal True + ] + + traversePatternTests : Test traversePatternTests = describe "traversePattern" diff --git a/tests/TagsTest.elm b/tests/TagsTest.elm new file mode 100644 index 0000000..ae83516 --- /dev/null +++ b/tests/TagsTest.elm @@ -0,0 +1,524 @@ +module TagsTest exposing (tests) + +import Expect exposing (Expectation) +import Review.Test +import Tags +import Test exposing (Test, describe, test) + + +tests : Test +tests = + describe "TagsTest tests" + [ commonTags + , commentsTags + , typesTags + , expressionTypeTags + , expressionTags + , destructuringTags + ] + + +commonTags : Test +commonTags = + let + data = + """ +[ + "paradigm:functional", + "technique:immutability", + "uses:module" +] +""" + in + describe "should always return the initialTags" + [ test "for an empty module" <| + \() -> + "module A exposing (..)" + |> Review.Test.run Tags.commonTagsRule + |> Review.Test.expectDataExtract data + , test "for an non-empty module" <| + \() -> + """ +module TwoFer exposing (twoFer) + +twoFer : Maybe String -> String +twoFer name = + "One for " + ++ Maybe.withDefault "you" name + ++ ", one for me." +""" + |> Review.Test.run Tags.commonTagsRule + |> Review.Test.expectDataExtract data + ] + + +commentsTags : Test +commentsTags = + describe "comments" + [ test "module documentation" <| + \() -> + """ +module A exposing (..) +{-| This a module documentation blurb +-} + +f x = x +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:comment\", \"construct:documentation\" ]" + , test "top level comment" <| + \() -> + """ +module A exposing (..) + +-- this is a top level comment +f x = x +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:comment\" ]" + , test "internal comment" <| + \() -> + """ +module A exposing (..) + +f x = + -- this is an internal comment + x +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:comment\" ]" + , test "function documentation" <| + \() -> + """ +module A exposing (..) + +{-| This is a function documentation comment +-} +f x = x +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:comment\", \"construct:documentation\" ]" + ] + + +typesTags : Test +typesTags = + describe "types" + [ test "type alias" <| + \() -> + """ +module A exposing (..) +type alias MyString = String +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"uses:type-alias\" ]" + , test "custom type" <| + \() -> + """ +module A exposing (..) +type MyType = MyType +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"uses:custom-type\" ]" + , test "union type" <| + \() -> + """ +module A exposing (..) +type MyType = A | B +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"uses:custom-type\", \"uses:union-type\" ]" + , test "generics type" <| + \() -> + """ +module A exposing (..) +type MyType a = MyType +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:generic-type\", \"uses:custom-type\" ]" + , test "direct recursive type" <| + \() -> + """ +module A exposing (..) +type MyType = A MyType +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:recursive-type\", \"uses:custom-type\" ]" + , test "nested recursive type" <| + \() -> + """ +module A exposing (..) +type MyType = A | B | C { c: (String, MyType) } +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:recursive-type\", \"uses:custom-type\", \"uses:union-type\" ]" + , test "nested generic recursive type" <| + \() -> + """ +module A exposing (..) +type MyType a x = A { c : ( String, { a | b : MyType a Int } ) } +""" + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract "[ \"construct:generic-type\", \"construct:recursive-type\", \"uses:custom-type\" ]" + ] + + +commonStart : String +commonStart = + """ +module A exposing (..) + +import Set +import Dict +import Random +import Regex +import Debug + """ + + +expectData : String -> String -> Expectation +expectData function data = + commonStart + ++ function + |> Review.Test.run Tags.expressionTagsRule + |> Review.Test.expectDataExtract data + + +expressionTypeTags : Test +expressionTypeTags = + describe "matching expression types" + [ test "Type constructors " <| + \() -> expectData "f = Just" "[ \"construct:constructor\"]" + , test "other functions and values too general for a tag" <| + \() -> expectData "f x = x" "[]" + , test "ParenthesizedExpression is too general for a tag" <| + \() -> expectData "f x = (x)" "[]" + , test "GLSLExpression" <| + \() -> expectData "f = [glsl| void main () {} |]" "[ \"uses:glsl\" ]" + , test "Application" <| + \() -> expectData "f x y = x y" "[ \"uses:function-application\" ]" + , test "PrefixOperator" <| + \() -> expectData "f = (^)" "[ \"uses:prefix-operator\" ]" + , test "using ()" <| + \() -> expectData "f = ()" "[ \"uses:unit\" ]" + , test "using float" <| + \() -> + expectData "f = 3.14" "[ \"construct:float\", \"construct:floating-point-number\" ]" + , test "using float scientific notation" <| + \() -> + expectData "f = 314e-2" "[ \"construct:float\", \"construct:floating-point-number\" ]" + , test "using int" <| + \() -> + expectData "f = 42" "[ \"construct:int\", \"construct:integral-number\" ]" + , test "using int with hex notation" <| + \() -> + expectData "f = 0x42" + "[ \"construct:hexadecimal-number\", \"construct:int\", \"construct:integral-number\" ]" + , test "using negation and int" <| + \() -> + expectData "f = -42" + "[ \"construct:int\", \"construct:integral-number\", \"construct:unary-minus\" ]" + , test "using a string literal" <| + \() -> + expectData "f = \"hello\"" "[ \"construct:string\" ]" + , test "using mutiline string literal" <| + \() -> + expectData "f = \"\"\"\nhello\nworld\"\"\"" + "[ \"construct:multiline-string\", \"construct:string\" ]" + , test "using lambda" <| + \() -> expectData "f x = (\\_ -> x)" "[ \"construct:lambda\" ]" + , test "using if block" <| + \() -> + expectData "f x y z = if x then y else z" + "[ \"construct:boolean\", \"construct:if\" ]" + , test "using let block" <| + \() -> + expectData "f x y = let z = y in z" "[ \"construct:assignment\" ]" + , test "using char" <| + \() -> expectData "f = 'a'" "[ \"construct:char\" ]" + , test "using tuple" <| + \() -> expectData "f a b = (a, b)" "[ \"construct:tuple\" ]" + , test "using list" <| + \() -> expectData "f a b = [a, b]" "[ \"construct:list\" ]" + , test "using case" <| + \() -> expectData "f a = case a of\n b -> b" "[ \"construct:pattern-matching\" ]" + , test "using record" <| + \() -> expectData "f b = {a = b}" "[ \"construct:record\" ]" + , test "using record access" <| + \() -> expectData "f rec = rec.field" "[ \"construct:record\", \"uses:record-access\" ]" + , test "using record access function" <| + \() -> + expectData "f = .field" + "[ \"construct:record\", \"uses:record-access\", \"uses:record-access-function\" ]" + , test "using record update" <| + \() -> expectData "f a = {a | b = a}" "[ \"construct:record\", \"uses:record-update\" ]" + ] + + +expressionTags : Test +expressionTags = + describe "matching expressions" + [ test "using Bitwise.and" <| + \() -> + expectData "f = Bitwise.and" + "[ \"construct:bit-manipulation\", \"construct:bitwise-and\" ]" + , test "using Bitwise.or" <| + \() -> + expectData "f = Bitwise.or" + "[ \"construct:bit-manipulation\", \"construct:bitwise-or\" ]" + , test "using Bitwise.xor" <| + \() -> + expectData "f = Bitwise.xor" + "[ \"construct:bit-manipulation\", \"construct:bitwise-xor\" ]" + , test "using Bitwise.complement" <| + \() -> + expectData "f = Bitwise.complement" + "[ \"construct:bit-manipulation\", \"construct:bitwise-not\" ]" + , test "using Bitwise.shiftLeftBy" <| + \() -> + expectData "f = Bitwise.shiftLeftBy" + "[ \"construct:bit-manipulation\", \"construct:bitwise-left-shift\" , \"technique:bit-shifting\"]" + , test "using Bitwise.shiftRightBy" <| + \() -> + expectData "f = Bitwise.shiftRightBy" + "[ \"construct:bit-manipulation\", \"construct:bitwise-right-shift\", \"technique:bit-shifting\" ]" + , test "using Bitwise.shiftRightZfBy" <| + \() -> + expectData "f = Bitwise.shiftRightZfBy" + "[ \"construct:bit-manipulation\", \"technique:bit-shifting\" ]" + , test "using Array function" <| + \() -> + expectData "f = Array.empty" + "[ \"construct:array\", \"technique:immutable-collection\" ]" + , test "using Bytes function" <| + \() -> expectData "f = Bytes.width" "[ \"construct:byte\" ]" + , test "using Bytes.Encode function" <| + \() -> expectData "f = Bytes.Encode.encode" "[ \"construct:byte\" ]" + , test "using List function" <| + \() -> expectData "f = List.all" "[ \"construct:list\" ]" + , test "using Set function" <| + \() -> + expectData "f = Set.empty" + "[ \"construct:set\", \"technique:immutable-collection\", \"technique:sorted-collection\" ]" + , test "using Time function" <| + \() -> expectData "f = Time.now" "[ \"construct:date-time\" ]" + , test "using Dict function" <| + \() -> + expectData "f = Dict.empty" + "[ \"construct:dictionary\", \"technique:immutable-collection\", \"technique:sorted-collection\" ]" + , test "using Random function" <| + \() -> + expectData "f = Random.constant" + "[ \"technique:randomness\" ]" + , test "using Regex function" <| + \() -> + expectData "f = Regex.fromString" + "[ \"technique:regular-expression\" ]" + , test "using inline +" <| + \() -> + expectData "f a b = a + b" + "[ \"construct:add\" , \"uses:function-application\"]" + , test "using prefix +" <| + \() -> + expectData "f = (+)" + "[ \"construct:add\", \"uses:prefix-operator\" ]" + , test "using inline -" <| + \() -> + expectData "f a b = a - b" + "[ \"construct:subtract\" , \"uses:function-application\"]" + , test "using prefix -" <| + \() -> + expectData "f = (-)" + "[ \"construct:subtract\", \"uses:prefix-operator\" ]" + , test "using inline *" <| + \() -> + expectData "f a b = a * b" + "[ \"construct:multiply\" , \"uses:function-application\"]" + , test "using prefix *" <| + \() -> + expectData "f = (*)" + "[ \"construct:multiply\", \"uses:prefix-operator\" ]" + , test "using inline /" <| + \() -> + expectData "f a b = a / b" + "[ \"construct:divide\" , \"uses:function-application\"]" + , test "using prefix /" <| + \() -> + expectData "f = (/)" + "[ \"construct:divide\", \"uses:prefix-operator\" ]" + , test "using inline //" <| + \() -> + expectData "f a b = a // b" + "[ \"construct:divide\" , \"uses:function-application\"]" + , test "using prefix //" <| + \() -> + expectData "f = (//)" + "[ \"construct:divide\", \"uses:prefix-operator\" ]" + , test "using inline ==" <| + \() -> + expectData "f a b = a == b" + "[ \"construct:boolean\", \"construct:equality\", \"technique:equality-comparison\" , \"uses:function-application\"]" + , test "using prefix ==" <| + \() -> + expectData "f = (==)" + "[ \"construct:boolean\", \"construct:equality\", \"technique:equality-comparison\", \"uses:prefix-operator\" ]" + , test "using inline /=" <| + \() -> + expectData "f a b = a /= b" + "[ \"construct:boolean\", \"construct:inequality\", \"technique:equality-comparison\" , \"uses:function-application\"]" + , test "using prefix /=" <| + \() -> + expectData "f = (/=)" + "[ \"construct:boolean\", \"construct:inequality\", \"technique:equality-comparison\", \"uses:prefix-operator\" ]" + , test "using inline &&" <| + \() -> + expectData "f a b = a && b" + "[ \"construct:boolean\", \"construct:logical-and\", \"technique:boolean-logic\", \"uses:function-application\"]" + , test "using prefix &&" <| + \() -> + expectData "f = (&&)" + "[ \"construct:boolean\", \"construct:logical-and\", \"technique:boolean-logic\", \"uses:prefix-operator\" ]" + , test "using inline ||" <| + \() -> + expectData "f a b = a || b" + "[ \"construct:boolean\", \"construct:logical-or\", \"technique:boolean-logic\", \"uses:function-application\"]" + , test "using prefix ||" <| + \() -> + expectData "f = (||)" + "[ \"construct:boolean\", \"construct:logical-or\", \"technique:boolean-logic\", \"uses:prefix-operator\" ]" + , test "using not" <| + \() -> + expectData "f x = not x" + "[ \"construct:boolean\", \"construct:logical-not\", \"technique:boolean-logic\", \"uses:function-application\" ]" + , test "using xor" <| + \() -> + expectData "f x y = xor x y" + "[ \"construct:boolean\", \"technique:boolean-logic\", \"uses:function-application\" ]" + , test "using True" <| + \() -> expectData "f = True" "[ \"construct:boolean\", \"construct:constructor\" ]" + , test "using False" <| + \() -> expectData "f = False" "[ \"construct:boolean\", \"construct:constructor\" ]" + , test "using isNaN" <| + \() -> + expectData "f x = isNaN x" + "[ \"construct:boolean\", \"construct:float\", \"construct:floating-point-number\", \"uses:function-application\" ]" + , test "using isInfinite" <| + \() -> + expectData "f x = isInfinite x" + "[ \"construct:boolean\", \"construct:float\", \"construct:floating-point-number\", \"uses:function-application\" ]" + ] + + +destructuringTags : Test +destructuringTags = + describe "destructuring" + [ test "let block without proper destructuring" <| + \() -> + expectData "f x y = let z = y in z" + "[ \"construct:assignment\" ]" + , test "let block with tuple destructuring" <| + \() -> + expectData "f y = let (a, b) = y in a" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with record destructuring" <| + \() -> + expectData "f y = let {a, b} = y in a" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with uncons destructuring" <| + \() -> + expectData "f y = let a :: b = y in a" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with named destructuring" <| + \() -> + expectData "f y = let Thing a = y in a" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with nested destructuring" <| + \() -> + expectData "f y = let (Thing a) = y in a" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with a function with record destructuring" <| + \() -> + expectData "f y = let f2 { a } = a in f2" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with a function with tuple destructuring" <| + \() -> + expectData "f y = let f2 (a, b) = a in b" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with a function with uncons destructuring" <| + \() -> + expectData "f y = let f2 (a :: b) = a in b" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "let block with a function with named destructuring" <| + \() -> + expectData "f y = let f2 (Thing a) = a in f2" + "[ \"construct:assignment\", \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "top-level function without proper destructuring" <| + \() -> + expectData "f x y = x" "[]" + , test "top-level function with tuple destructuring" <| + \() -> + expectData "f (a, b) = a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "top-level function with record destructuring" <| + \() -> + expectData "f {a, b} = a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "top-level function with uncons destructuring" <| + \() -> + expectData "f (a :: b) = a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "top-level function with named destructuring" <| + \() -> + expectData "f (Thing a) = a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "top-level function with nested destructuring" <| + \() -> + expectData "f (Thing { a }) = a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "lambda without proper destructuring" <| + \() -> + expectData "f = \\x y -> x" "[\"construct:lambda\"]" + , test "lambda with tuple destructuring" <| + \() -> + expectData "f = \\(a, b) -> a" + "[ \"construct:destructuring\", \"construct:lambda\", \"construct:pattern-matching\"]" + , test "lambda with record destructuring" <| + \() -> + expectData "f = \\{a, b} -> a" + "[ \"construct:destructuring\", \"construct:lambda\", \"construct:pattern-matching\"]" + , test "lambda with uncons destructuring" <| + \() -> + expectData "f = \\(a :: b) -> a" + "[ \"construct:destructuring\", \"construct:lambda\", \"construct:pattern-matching\"]" + , test "lambda with named destructuring" <| + \() -> + expectData "f = \\(Thing a) -> a" + "[ \"construct:destructuring\", \"construct:lambda\", \"construct:pattern-matching\"]" + , test "lambda with nested destructuring" <| + \() -> + expectData "f = \\(Thing { a }) -> a" + "[ \"construct:destructuring\", \"construct:lambda\", \"construct:pattern-matching\"]" + , test "case without proper destructuring" <| + \() -> + expectData "f x = case x of\n x -> x" "[\"construct:pattern-matching\"]" + , test "case with tuple destructuring" <| + \() -> + expectData "f x = case x of\n (a, b) -> a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "case with record destructuring" <| + \() -> + expectData "f x = case x of\n {a, b} -> a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "case with uncons destructuring" <| + \() -> + expectData "f x = case x of\n (a :: b) -> a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "case with named destructuring" <| + \() -> + expectData "f x = case x of\n (Thing a) -> a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + , test "case with nested destructuring" <| + \() -> + expectData "f x = case x of\n (Thing { a }) -> a" + "[ \"construct:destructuring\", \"construct:pattern-matching\"]" + ]