diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 446a573b..ee3cfafc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -116,4 +116,46 @@ jobs: run: ./gradlew --full-stacktrace publishPlugins - name: Stop Gradle daemons - run: ./gradlew --stop \ No newline at end of file + run: ./gradlew --stop + + publish_documentation: + needs: build + runs-on: ubuntu-latest + + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_CLOUDFRONT_ID: ${{ secrets.AWS_CLOUDFRONT_ID }} + AWS_DEFAULT_REGION: eu-west-1 + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + JEKYLL_ENV: production + S3_BUCKET: ${{ secrets.S3_BUCKET }} + + steps: + - uses: actions/checkout@v3.0.0 + + - uses: actions/setup-java@v3.0.0 + with: + distribution: 'adopt' + java-version: '15' + cache: 'gradle' + + - name: Prepare environment + run: | + sudo gem install bundler + bundle config set --local path 'vendor/bundle' + bundle install --gemfile docs/Gemfile + + - name: Create API Doc and validate + run: ./gradlew buildDoc + + - name: Generate site + run: bundle exec jekyll build -b docs/analysis -s docs/docs -d docs/build/_site + + - name: Upload site to S3 bucket + run: aws s3 sync --delete docs/build/_site s3://$S3_BUCKET/docs/analysis + + - name: Invalidate CloudFront cache + run: aws cloudfront create-invalidation --distribution-id $AWS_CLOUDFRONT_ID --paths "/docs/analysis" + + - name: Stop Gradle daemons + run: ./gradlew --stop \ No newline at end of file diff --git a/README.md b/README.md index e1e7d17a..00485dfb 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,18 @@ # Λrrow Analysis -[![Kotlin version badge](https://img.shields.io/badge/kotlin-1.6-blue.svg)](https://kotlinlang.org/docs/whatsnew16.html) +[![Kotlin version badge](https://img.shields.io/badge/kotlin-1.6.20-blue.svg)](https://kotlinlang.org/docs/whatsnew16.html) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Maven Central](https://img.shields.io/maven-central/v/io.arrow-kt/arrow-analysis-common?color=4caf50&label=latest%20release)](https://maven-badges.herokuapp.com/maven-central/io.arrow-kt/arrow-analysis-common) ## Contributing -Λrrow Meta is an inclusive community powered by awesome individuals like you. As an actively growing ecosystem, Λrrow Meta and its associated libraries and toolsets are in need of new contributors! We have issues suited for all levels, from entry to advanced, and our maintainers are happy to provide 1:1 mentoring. All are welcome in Λrrow Meta. - -If you’re looking to contribute, have questions, or want to keep up-to-date about what’s happening, please follow us here and say hello! +Λrrow is an inclusive community powered by awesome individuals like you. If you’re looking to contribute, have questions, or want to keep up-to-date about what’s happening, please follow us here and say hello! - [#arrow-meta on Kotlin Slack](https://kotlinlang.slack.com/) -## Licence +## License ``` -Copyright (C) 2017 The Λrrow Authors +Copyright (C) 2017 - 2022 The Λrrow Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle.kts b/build.gradle.kts index c9b75e27..1d977d97 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,22 @@ allprojects { group = property("projects.group").toString() } +tasks { + create("generateDoc") { + commandLine("sh", "gradlew", "dokkaJekyll") + } + + create("buildDoc") { + group = "documentation" + description = "Generates API Doc and validates all the documentation" + dependsOn("generateDoc") + } +} + +allprojects { + extra.set("dokka.outputDirectory", rootDir.resolve("docs/docs/apidocs")) +} + allprojects { this.tasks.withType() { useJUnitPlatform() diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..d6f8c3d4 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,15 @@ +build + +## Jekyll +_site +.sass-cache +.jekyll-metadata +.jekyll-cache + +## OSX bullshit +.DS_Store + +## Ruby environment normalization: +/.bundle/ +/vendor/bundle +/lib/bundler/man/ diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000..bf684b9a --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.2.0" +gem "kramdown", ">= 2.3.1" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 00000000..d4817d74 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,73 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + colorator (1.1.0) + concurrent-ruby (1.1.8) + em-websocket (0.5.2) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0.6.0) + eventmachine (1.2.7) + eventmachine (1.2.7-x64-mingw32) + ffi (1.15.0) + ffi (1.15.0-x64-mingw32) + forwardable-extended (2.6.0) + http_parser.rb (0.6.0) + i18n (1.8.10) + concurrent-ruby (~> 1.0) + jekyll (4.2.0) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (~> 2.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (~> 0.4.0) + pathutil (~> 0.9) + rouge (~> 3.0) + safe_yaml (~> 1.0) + terminal-table (~> 2.0) + jekyll-sass-converter (2.1.0) + sassc (> 2.0.1, < 3.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + kramdown (2.3.1) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.3) + listen (3.5.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (4.0.6) + rb-fsevent (0.10.4) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.5) + rouge (3.26.0) + safe_yaml (1.0.5) + sassc (2.4.0) + ffi (~> 1.9) + sassc (2.4.0-x64-mingw32) + ffi (~> 1.9) + terminal-table (2.0.0) + unicode-display_width (~> 1.1, >= 1.1.1) + unicode-display_width (1.7.0) + +PLATFORMS + ruby + x64-mingw32 + +DEPENDENCIES + jekyll (~> 4.2.0) + kramdown (>= 2.3.1) + +BUNDLED WITH + 2.1.4 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..984745a2 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,38 @@ +--- +layout: docs +title: Async +permalink: /docs/effects/async/ +--- + +# Documentation + +## Prerequisites + +
+ System requirements: +

+ + _You just need to do this one time in case you have problems building the site_ +

+
+ +* `ruby` >= 2.4.0 (check [rvm.io](https://rvm.io/) on the best way to install `ruby` on your system) +* `rubygems` >= 2.5.0 (`gem update --system` will update your `ruby` ecosystem) +* `bundler` >= 2.0.0 (`gem install bundler` will install the latest version) + + + +## Build and run in your local environment + +Run `build-and-run-website.sh` script which is located in `scripts` directory. + +It can be executed from any directory: + +``` +$> ./scripts/build-and-run-website.sh +``` + +``` +$> cd scripts +$> ./build-and-run-website.sh +``` diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts new file mode 100644 index 00000000..fabb8da3 --- /dev/null +++ b/docs/build.gradle.kts @@ -0,0 +1,25 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id(libs.plugins.kotlin.jvm.get().pluginId) + alias(libs.plugins.arrowGradleConfig.kotlin) +} + +dependencies { + runtimeOnly(libs.kotlin.stdlibJDK8) + runtimeOnly(projects.arrowAnalysisTypes) +} + +tasks { + named("clean") { + delete("$rootDir/docs/docs/apidocs") + } + + compileKotlin { + kotlinOptions.freeCompilerArgs += listOf("-Xskip-runtime-version-check") + } +} diff --git a/docs/docs/_config.yml b/docs/docs/_config.yml new file mode 100644 index 00000000..57d24bbe --- /dev/null +++ b/docs/docs/_config.yml @@ -0,0 +1,32 @@ +title: Arrow Analysis +#------------------------- +name: Λrrow Analysis +#------------------------- +description: Pre-, post-condition, and invariant checks for your Kotlin code +#------------------------- +author: The Λrrow Authors +keywords: functional-programming, kotlin-library, static-analysis, contracts +#------------------------- +url: https://arrow-kt.io/docs/analysis +#------------------------- +arrow_url: https://arrow-kt.io +#------------------------- +github_repo: https://github.com/arrow-kt/arrow-analysis +#------------------------- +twitter_handle: "@arrow_kt" +#------------------------- +slack_kotlin_channel: "https://slack.kotlinlang.org" +slack_arrow_channel: "https://kotlinlang.slack.com/archives/C5UPMM0A0" +#------------------------- +markdown: kramdown +sass: + style: compressed +#------------------------- +exclude: ['config.ru', 'Gemfile', 'Gemfile.lock', 'vendor', 'Procfile', 'Rakefile', 'build', 'build.gradle.kts', 'gradle.properties'] +#------------------------- +defaults: + - scope: + path: "apidocs" + type: "pages" + values: + layout: "docs-analysis" diff --git a/docs/docs/_data/analysis/features.yml b/docs/docs/_data/analysis/features.yml new file mode 100644 index 00000000..9abe4dbf --- /dev/null +++ b/docs/docs/_data/analysis/features.yml @@ -0,0 +1,15 @@ +content: + - title: Quickstart + url: https://arrow-kt.io/docs/quickstart/ + + - title: Core + url: https://arrow-kt.io/docs/core/ + + - title: FX + url: https://arrow-kt.io/docs/fx/ + + - title: Optics + url: https://arrow-kt.io/docs/optics/dsl/ + + - title: Meta + url: https://arrow-kt.io/docs/meta/ diff --git a/docs/docs/_data/analysis/sidebar.yml b/docs/docs/_data/analysis/sidebar.yml new file mode 100644 index 00000000..372cc743 --- /dev/null +++ b/docs/docs/_data/analysis/sidebar.yml @@ -0,0 +1,30 @@ +options: + - title: Quick Start + url: / + + - title: Pre and post-conditions + url: /conditions + + - title: Control operators (if, when) + url: /control + + - title: Mutability and loops + url: /mutability + + - title: Types and invariants + url: /types + + - title: Wrappers + url: /wrappers + + - title: Fields + url: /fields + + - title: 3rd-party libraries + url: /laws + + - title: Java support + url: /java + + - title: GitHub Actions / SARIF + url: /sarif \ No newline at end of file diff --git a/docs/docs/_includes/_doc.html b/docs/docs/_includes/_doc.html new file mode 100644 index 00000000..02d7c497 --- /dev/null +++ b/docs/docs/_includes/_doc.html @@ -0,0 +1,27 @@ +
+
+ +
+ Search + +
+ +
+
+ {% if page.video %} + {% include _media-wrapper.html id=page.video %} + {% endif %} + {{ content }} +
+
diff --git a/docs/docs/_includes/_github-modal.html b/docs/docs/_includes/_github-modal.html new file mode 100644 index 00000000..e488b7e8 --- /dev/null +++ b/docs/docs/_includes/_github-modal.html @@ -0,0 +1,18 @@ +
+
+

Do you like Arrow?

+ +
+ Arrow Org + +
diff --git a/docs/docs/_includes/_head-docs-analysis.html b/docs/docs/_includes/_head-docs-analysis.html new file mode 100644 index 00000000..7be7679e --- /dev/null +++ b/docs/docs/_includes/_head-docs-analysis.html @@ -0,0 +1,33 @@ + + + Λrrow Analysis + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/docs/_includes/_js-bottom-docs.html b/docs/docs/_includes/_js-bottom-docs.html new file mode 100644 index 00000000..f39c727c --- /dev/null +++ b/docs/docs/_includes/_js-bottom-docs.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/docs/docs/_includes/_media-wrapper.html b/docs/docs/_includes/_media-wrapper.html new file mode 100644 index 00000000..a018a9dd --- /dev/null +++ b/docs/docs/_includes/_media-wrapper.html @@ -0,0 +1,16 @@ + + Watch video + + +
+

Video

+
+
+
+ +
+
+
+
diff --git a/docs/docs/_includes/_sidebar-analysis.html b/docs/docs/_includes/_sidebar-analysis.html new file mode 100644 index 00000000..18732e1a --- /dev/null +++ b/docs/docs/_includes/_sidebar-analysis.html @@ -0,0 +1,48 @@ + \ No newline at end of file diff --git a/docs/docs/_includes/_sidebar-cat-dropdown-analysis.html b/docs/docs/_includes/_sidebar-cat-dropdown-analysis.html new file mode 100644 index 00000000..092d43ba --- /dev/null +++ b/docs/docs/_includes/_sidebar-cat-dropdown-analysis.html @@ -0,0 +1,19 @@ +
+ +
\ No newline at end of file diff --git a/docs/docs/_includes/_slack-link.html b/docs/docs/_includes/_slack-link.html new file mode 100644 index 00000000..a4ec29e7 --- /dev/null +++ b/docs/docs/_includes/_slack-link.html @@ -0,0 +1,22 @@ +
<
+ diff --git a/docs/docs/_layouts/docs-analysis.html b/docs/docs/_layouts/docs-analysis.html new file mode 100644 index 00000000..c325cd89 --- /dev/null +++ b/docs/docs/_layouts/docs-analysis.html @@ -0,0 +1,17 @@ + + +{% include _head-docs-analysis.html %} + + +
+ {% include _sidebar-analysis.html %} + +
+ {% include _github-modal.html %} + {% include _slack-link.html %} + {% include _js-bottom-docs.html %} + + + diff --git a/docs/docs/_layouts/error.html b/docs/docs/_layouts/error.html new file mode 100644 index 00000000..e70fcbd0 --- /dev/null +++ b/docs/docs/_layouts/error.html @@ -0,0 +1,47 @@ + + + + + + {{site.data.commons.name}} + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ Code error +
+
+

404

+

Resource not found

+ Back to home +
+
+
+
+ + + diff --git a/docs/docs/_sass/base/_base.scss b/docs/docs/_sass/base/_base.scss new file mode 100644 index 00000000..3ebafd82 --- /dev/null +++ b/docs/docs/_sass/base/_base.scss @@ -0,0 +1,69 @@ +// Base +// ----------------------------------------------- +// ----------------------------------------------- +// Body, html +// ----------------------------------------------- + +html { + box-sizing: border-box; + font-size: $base-font-size; +} + +*, +*::after, +*::before { + box-sizing: inherit; +} + +body, +html { + height: 100%; + transition: background-color 300ms $base-timing; +} +// Typography +// ----------------------------------------------- + +body { + font-family: $base-font-family; + line-height: $base-line-height; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: $header-font-color; + font-family: $header-font-family; + font-weight: $font-semibold; +} + +a { + color: $brand-primary; + text-decoration: none; + transition: color $base-duration $base-timing; + + &:visited { + color: $brand-primary; + } + + &:hover { + color: $link-hover; + text-decoration: underline; + } + + &:active { + color: $brand-primary; + } +} + +hr { + display: block; + border: none; +} + +// Gitter action bar fix +.gitter-chat-embed-action-bar { + z-index: 11; +} diff --git a/docs/docs/_sass/base/_fonts.scss b/docs/docs/_sass/base/_fonts.scss new file mode 100644 index 00000000..b9000560 --- /dev/null +++ b/docs/docs/_sass/base/_fonts.scss @@ -0,0 +1,29 @@ +// Iosevka Fonts +// ----------------------------------------------- +@font-face { + font-family: 'Iosevka Web'; + font-weight: 400; + font-style: normal; + src: url("../fonts/woff2/iosevka-regular.woff2") format('woff2'), url("../fonts/woff/iosevka-regular.woff") format('woff'), url("../fonts/ttf/iosevka-regular.ttf") format('truetype'); +} +@font-face { + font-family: 'Iosevka Web'; + font-weight: 500; + font-style: normal; + src: url("../fonts/woff2/iosevka-medium.woff2") format('woff2'), url("../fonts/woff/iosevka-medium.woff") format('woff'), url("../fonts/ttf/iosevka-medium.ttf") format('truetype'); +} +@font-face { + font-family: 'Iosevka Web'; + font-weight: 600; + font-style: normal; + src: url("../fonts/woff2/iosevka-semibold.woff2") format('woff2'), url("../fonts/woff/iosevka-semibold.woff") format('woff'), url("../fonts/ttf/iosevka-semibold.ttf") format('truetype'); +} +@font-face { + font-family: 'Iosevka Web'; + font-weight: 700; + font-style: normal; + src: url("../fonts/woff2/iosevka-bold.woff2") format('woff2'), url("../fonts/woff/iosevka-bold.woff") format('woff'), url("../fonts/ttf/iosevka-bold.ttf") format('truetype'); +} +// Google Fonts +// ----------------------------------------------- +@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=swap'); diff --git a/docs/docs/_sass/base/_helpers.scss b/docs/docs/_sass/base/_helpers.scss new file mode 100644 index 00000000..34f6ca7d --- /dev/null +++ b/docs/docs/_sass/base/_helpers.scss @@ -0,0 +1,58 @@ +// Helpers +// ----------------------------------------------- +// ----------------------------------------------- + +.wrapper { + padding: ($base-point-grid * 2) ($base-point-grid * 3); + margin: 0 auto; + box-sizing: border-box; + max-width: $bp-xlarge; +} + +// Github stars +.doc-stars-container { + margin-left: ($base-point-grid * 2); +} + +// Github stars modal +#star-modal { + transition: transform $base-duration; + transform: scale(0); + border-radius: 2px; + position: fixed; + bottom: ($base-point-grid * 11); + right: ($base-point-grid * 2); + padding: $base-point-grid; + background: #fff; + box-shadow: $box-shadow-button; + z-index: 100; + min-width: $github-modal-width; + + &.in { + transform: scale(1); + } + + &.out { + transform: translateX(400px); + } + p { + font-size: $base-font-size; + margin: 0 0 15px; + } + & .close-modal { + cursor: pointer; + padding: 0 10.5px; + width: $text-box-icon-size; + height: $text-box-icon-size; + border-radius: ($base-point-grid * 3); + + &:hover { + background-color: lighten($brand-tertiary, 15%); + } + } + + .stars-text-line { + display: flex; + justify-content: space-between; + } +} diff --git a/docs/docs/_sass/base/_reset.scss b/docs/docs/_sass/base/_reset.scss new file mode 100644 index 00000000..76220f63 --- /dev/null +++ b/docs/docs/_sass/base/_reset.scss @@ -0,0 +1,141 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ +a, +abbr, +acronym, +address, +applet, +article, +aside, +audio, +b, +big, +blockquote, +body, +canvas, +caption, +center, +cite, +code, +dd, +del, +details, +dfn, +div, +dl, +dt, +em, +embed, +fieldset, +figcaption, +figure, +footer, +form, +h1, +h2, +h3, +h4, +h5, +h6, +header, +hgroup, +html, +i, +iframe, +img, +ins, +kbd, +label, +legend, +li, +mark, +menu, +nav, +object, +ol, +output, +p, +pre, +q, +ruby, +s, +samp, +section, +small, +span, +strike, +strong, +sub, +summary, +sup, +table, +tbody, +td, +tfoot, +th, +thead, +time, +tr, +tt, +u, +ul, +var, +video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} + +body { + line-height: 1; +} + +ol, +ul { + list-style: none; +} + +blockquote, +q { + quotes: none; +} + +blockquote { + &:after, + &:before { + content: ''; + content: none; + } +} + +q { + &:after, + &:before { + content: ''; + content: none; + } +} + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/docs/docs/_sass/components/_button.scss b/docs/docs/_sass/components/_button.scss new file mode 100644 index 00000000..e8b6d8e5 --- /dev/null +++ b/docs/docs/_sass/components/_button.scss @@ -0,0 +1,129 @@ +// Buttons +// ---------------------------------------------- +// ---------------------------------------------- + +.button { + display: block; + background: none; + border: none; + outline: none; + text-decoration: none; + position: relative; + + &:hover { + cursor: pointer; + } + + > img { + vertical-align: bottom; + } +} + +.error-button { + display: block; + background: none; + border: solid 1px; + padding: ($base-point-grid) ($base-point-grid * 3); + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; +} + +.close { + height: 28px; + position: absolute; + left: 0; + top: 0; + width: 32px; + + &::after, + &::before { + background-color: #fff; + content: " "; + height: 100%; + left: 98%; + position: absolute; + top: 50%; + width: 2px; + } + + &::before { + transform: rotate(45deg); + } + + &::after { + transform: rotate(-45deg); + } +} + +// Slack channel link button +.slack-link-container { + position: fixed; + bottom: $base-point-grid; + right: ($base-point-grid * 2); + padding: 0.5em 0.5em; + min-width: $github-modal-width; + border-radius: .2em; + font-size: 12px; + text-transform: uppercase; + text-align: center; + text-decoration: none; + transition: right .3s ease; + box-shadow: $box-shadow-button; + background: $white; + font-weight: $font-semibold; + color: $box-text-color; + + .slack-link { + color: lighten($box-text-color, 25%); + + &:hover { + color: $box-text-color; + } + } + + .slack-head-line { + display: flex; + justify-content: space-between; + } +} + +.hide-slack-icon { + font-size: $base-font-size; + cursor: pointer; + width: $text-box-icon-size; + height: $text-box-icon-size; + border-radius: ($base-point-grid * 3); + margin-right: 2px; + + &:hover { + background-color: lighten($brand-tertiary, 15%); + } +} + +.show-slack-icon { + color: $box-text-color; + font-weight: $font-semibold; + font-size: $base-font-size; + cursor: pointer; + position: fixed; + right: -80px; + bottom: 37px; + background-color: lighten($brand-tertiary, 15%); + border-radius: ($base-point-grid * 3) 0 0 ($base-point-grid * 3); + transition: padding .2s ease, right .2s ease, left .2s ease; + padding: 5px ($base-point-grid * 2); + box-shadow: 0 1px 1px 0 rgba($gray-primary,.3),0 1px 3px 1px rgba($gray-primary,.15); + + &:hover { + padding-right: ($base-point-grid * 3); + } +} + +.show-slack-icon-visible { + right: 0; +} + +.slack-text-out { + right: -200px; +} diff --git a/docs/docs/_sass/components/_code.scss b/docs/docs/_sass/components/_code.scss new file mode 100644 index 00000000..9e78789a --- /dev/null +++ b/docs/docs/_sass/components/_code.scss @@ -0,0 +1,48 @@ +// Code +// ----------------------------------------------- +// ----------------------------------------------- + +p, +ul, +ol { + code { + font-family: $code-font-family; + padding: 0 $base-point-grid ($base-point-grid - 4) $base-point-grid; + font-size: 16px; + border-radius: 3px; + color: rgba(#263238, 0.7); + } +} + + +.doc-content { + + + .CodeMirror, + .hljs { + font-size: 15px; + font-family: $code-font-family; + + pre { + line-height: 1.8; + } + + } + + .cm-s-arrow span.cm-keyword { + font-weight: $font-regular; + } + + pre { + + &.highlight { + border: 1px solid $code-border-color; + margin-bottom: $code-margin; + } + } +} + + +.language-kotlin { + display: none; +} diff --git a/docs/docs/_sass/components/_doc-body.scss b/docs/docs/_sass/components/_doc-body.scss new file mode 100644 index 00000000..52317870 --- /dev/null +++ b/docs/docs/_sass/components/_doc-body.scss @@ -0,0 +1,6 @@ +// DOC-BODY +// ----------------------------------------------- +// ----------------------------------------------- +#doc-body { + background: $white; +} diff --git a/docs/docs/_sass/components/_doc-content-meta.scss b/docs/docs/_sass/components/_doc-content-meta.scss new file mode 100644 index 00000000..443590b6 --- /dev/null +++ b/docs/docs/_sass/components/_doc-content-meta.scss @@ -0,0 +1,44 @@ +// DOC CONTENT META +// ----------------------------------------------- +// ----------------------------------------------- + +.doc-content-meta { + .doc-content { + a { + color: $meta-color; + } + + p, + ul { + code { + background: rgba($meta-background, 0.08); + } + } + + .cm-s-arrow span.cm-keyword { + color: $meta-color; + } + + .hljs-variable, + .hljs-template-variable, + .hljs-tag, + .hljs-name, + .hljs-selector-id, + .hljs-selector-class, + .hljs-regexp, + .hljs-deletion { + color: $meta-color; + } + + .button-video { + background: $white; + border: 2px solid $meta-color; + transition: background $base-duration; + + &:hover { + background: $meta-color; + color: $white; + } + } + } +} diff --git a/docs/docs/_sass/components/_doc-content.scss b/docs/docs/_sass/components/_doc-content.scss new file mode 100644 index 00000000..66bb3091 --- /dev/null +++ b/docs/docs/_sass/components/_doc-content.scss @@ -0,0 +1,190 @@ +// DOC-CONTENT +// ----------------------------------------------- +// ----------------------------------------------- + +.doc-content { + font-family: $doc-font-family; + position: relative; + padding: ($base-point-grid * 4); + font-size: 1.125rem; + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-bottom: ($base-point-grid * 2); + margin-top: ($base-point-grid * 4); + color: $header-font-color; + + &:first-child { + margin-top: 0; + } + } + + h1 { + font-size: 2.25rem; + } + + h2 { + font-size: 1.9rem; + } + + h3 { + font-size: 1.675rem; + } + + h4 { + font-size: 1.425rem; + } + + h5 {} + + h6 {} + + p { + margin-bottom: ($base-point-grid * 2); + } + + .browser { + margin-top: ($base-point-grid * 3); + } + + ul, ol { + padding-left: ($base-point-grid * 3); + margin-bottom: ($base-point-grid * 2); + } + + ul { + + li { + list-style: circle; + } + } + + ol { + li { + list-style-type: decimal; + } + } + + strong { + font-weight: $font-bold; + } + + del { + text-decoration: line-through; + } + + em { + font-style: italic; + } + + hr { + border: none; + height: 1px; + width: 100%; + background: rgba($brand-secondary, 0.9); + } + + table { + width: 100%; + thead { + text-align: left; + font-weight: bold; + } + + th, td { + padding: $base-point-grid; + border: 1px solid rgba($brand-secondary, 0.9); + } + + tr:nth-child(2n) { + background: $background-color; + } + + tbody { + tr { + td > code:first-of-type:before { + content: '\A'; + white-space: pre; + } + } + } + + } + + .header-link { + float: left; + font-size: 1.4rem; + line-height: 1; + margin-left: -24px; + margin-top: 10px; + opacity: 0; + transition: opacity $base-duration $base-timing; + } + + h1:hover, + h2:hover, + h3:hover, + h4:hover, + h5:hover, + h6:hover { + .header-link { + opacity: 1; + } + } + + .gif { + img { + max-width: 100%; + } + } + + .setup-gradle { + margin-top: 1rem; + margin-bottom: 1rem; + background: #fff; + box-shadow: $base-box-shadow; + + .tab { + overflow: hidden; + p { + display: grid; + grid-template-columns: repeat(3, 1fr); + button:nth-child(2) { + border-right: 1px solid #DCDEE1; + border-left: 1px solid #DCDEE1; + } + } + } + + .tab button { + background-color: rgba($dark-color-doc, 0.03); + border: none; + outline: none; + cursor: pointer; + padding: 1.75rem 1rem; + font-size: 1.25rem; + font-family: $heading-font-family; + font-weight: $font-bold; + transition: 0.3s; + box-shadow: 0 1px #DCDEE1; + } + /* Change background color of buttons on hover */ + .tab button:hover { + background-color: rgba($dark-color-doc, 0.01); + } + /* Create an active/current tablink class */ + .tab button.active { + background: none; + box-shadow: none; + } + /* Style the tab content */ + .tabcontent { + display: none; + padding: 0rem 1rem 0.1rem 1rem; + } + } +} diff --git a/docs/docs/_sass/components/_doc-header.scss b/docs/docs/_sass/components/_doc-header.scss new file mode 100644 index 00000000..0d36a7b3 --- /dev/null +++ b/docs/docs/_sass/components/_doc-header.scss @@ -0,0 +1,101 @@ +// DOC HEADER +// ----------------------------------------------- +// ----------------------------------------------- + +.doc-header { + display: flex; + align-items: center; + height: ($base-point-grid * 8); + background: $white; + border-bottom: 1px solid $doc-header-border; + + .search-wrapper { + display: flex; + position: relative; + width: 100%; + + .search { + display: block; + width: calc(100% - 1.5rem); + padding: $base-point-grid 0; + font-family: $base-font-family; + font-size: $base-docs-font-size; + margin-left: 1.5rem; + border: none; + cursor: auto; + position: relative; + outline: none; + -webkit-appearance: textfield; + } + + /* clears the 'X' from Internet Explorer */ + input[type=search]::-ms-clear { + display: none; + width: 0; + height: 0; + } + + input[type=search]::-ms-reveal { + display: none; + width: 0; + height: 0; + } + + /* clears the 'X' from Chrome */ + input[type="search"]::-webkit-search-decoration, + input[type="search"]::-webkit-search-cancel-button, + input[type="search"]::-webkit-search-results-button, + input[type="search"]::-webkit-search-results-decoration { + display: none; + } + } + + a { + &:hover { + text-decoration: none; + } + } + + .fa { + margin-right: ($base-point-grid / 2); + } + + .menu-doc-search { + display: inline-flex; + + .menu-links { + padding-bottom: 4px; + text-transform: uppercase; + font-weight: $font-semibold; + @include links($link-color, $link-color, lighten($link-color, 30%), lighten($link-color, 30%)); + + &:hover { + text-decoration: none; + } + } + &:last-child { + margin-right: ($base-point-grid * 2); + } + } +} + +// Responsive +@include bp(large) { + #doc-wrapper { + .doc-header { + .doc-stars-container { + display: none; + } + } + } +} + +@include bp(medium) { + #doc-wrapper { + .doc-header { + position: sticky; + top: 0; + z-index: 11; + } + } +} diff --git a/docs/docs/_sass/components/_dropdown.scss b/docs/docs/_sass/components/_dropdown.scss new file mode 100644 index 00000000..b5de2d5c --- /dev/null +++ b/docs/docs/_sass/components/_dropdown.scss @@ -0,0 +1,497 @@ +// Detect lightness +@function detectLightness($color) { + @if (lightness($color) > 60) { + @return mix($color, #000, 90%); + } @else { + @return mix($color, #fff, 90%); + } +} +// Spacing +@function spacing($type) { + @if $type == 'compact' { + @return 12px; + } + + @if $type == 'spacious' { + @return 22px; + } @else { + @return 16px; + } +} +// Description +@function desc($display) { + @if $display == false { + @return none; + } @else { + @return block; + } +} +// Layout type +@mixin layout-width($type) { + @if $type == 'small' { + max-width: 500px; + min-width: 400px; + } + + @if $type == 'normal' { + max-width: 600px; + min-width: 500px; + } + + @if $type == 'large' { + max-width: 800px; + min-width: 600px; + } + + @if $type == 'full' { + width: calc(100% + 55px); + max-width: 900px; + } +} +// Mixin - Shadow type +@mixin shadow-type($type) { + @if $type == 'light' { + box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1); + } + + @if $type == 'heavy' { + box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); + } + + @if $type == 'none' { + box-shadow: none; + } +} +// Mixin - highlight +@function set-highlight($highlight, $color) { + @if $highlight == 1 and lightness($color) < 60 { + @return mix(#fff, $color, 90%); + } @else { + @return darken(detectLightness($color), 30%); + } +} + +@mixin dropdown( +$main-color: #458EE1, +$layout-width: normal, +$layout-type: normal, +$background-color: #FFFFFF, +$border-radius: 4, +$border-width: 1, +$border-color: #d9d9d9, +$box-shadow: light, +$branding-position: bottom, +$font-size: normal, +$header-color: #33363D , +$title-color: #02060C, +$subtitle-color: #A4A7AE, +$text-color: #63676D, +$highlight-color: #3881FF, +$spacing: normal, +$include-desc: true, +$background-category-header: #FFFFFF, +$highlight-opacity: .1, +$highlight-type: 'underline', +$code-background: #EBEBEB, +$responsive-breakpoint: 768px +){ + + $header-size: 1em; + $title-size: .9em; + $text-size: .85em; + $subtitle-size: .9em; + $padding: spacing($spacing); + + @if $font-size == 'small' { + $header-size: 0.95em; + $title-size: 0.8em; + $text-size: 0.75em; + $subtitle-size: 0.8em; + } @else + if $font-size == 'large' { + $header-size: 1.1em; + $title-size: 1em; + $text-size: 0.9em; + $subtitle-size: 1em; + } + + .algolia-autocomplete { + width: 100%; + + .ds-dropdown-menu { + left: 0 !important; + right: inherit !important; + + &:before { + left: 24px; + } + } + // Dropdown wrapper + .ds-dropdown-menu { + position: relative; + top: -6px; + border-radius: $border-radius+px; + margin: 8px 0 0; + padding: 0; + text-align: left; + height: auto; + position: relative; + background: transparent; + border: none; + z-index: 999; + @include layout-width($layout-width); + @include shadow-type($box-shadow); + // Arrow + &:before { + display: block; + position: absolute; + content: ''; + width: 20px; + height: 20px; + background: $background-color; + z-index: 1000; + top: -10px; + border-top: $border-width+px solid $border-color; + border-right: $border-width+px solid $border-color; + transform: rotate(-45deg); + border-radius: 2px; + } + + .ds-suggestions { + position: relative; + z-index: 1000; + margin-top: $padding/2; + } + + .ds-suggestion { + cursor: pointer; + + &.ds-cursor { + .algolia-docsearch-suggestion.suggestion-layout-simple { + background-color: rgba($main-color,.2); + } + + .algolia-docsearch-suggestion:not(.suggestion-layout-simple) { + .algolia-docsearch-suggestion--content { + background-color: rgba($main-color,.2); + } + } + } + } + + [class^="ds-dataset-"] { + position: relative; + border: solid $border-width+px $border-color; + background: $background-color; + border-radius: $border-radius+px; + overflow: auto; + padding: 0 $padding/2 $padding/2; + } + // Inner-grid setup + * { + box-sizing: border-box; + } + } + // Each suggestion item is wrapped + .algolia-docsearch-suggestion { + position: relative; + padding: 0 $padding/2; + background: $background-color; + color: $title-color; + overflow: hidden; + @if $highlight-type == basic { + &--highlight { + color: $main-color; + background-color: rgba($main-color, $highlight-opacity); + } + + .algolia-docsearch-suggestion--item-header .algolia-docsearch-suggestion--highlight { + color: inherit; + background: inherit; + } + } @else { + &--highlight { + color: set-highlight($highlight-opacity, $main-color); + background: rgba(mix($main-color, #fff, 60%), $highlight-opacity); + padding: 0.1em 0.05em; + } + + &--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight, + &--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight { + color: inherit; + background: inherit; + } + + &--text .algolia-docsearch-suggestion--highlight { + padding: 0 0 1px; + background: inherit; + box-shadow: inset 0 -2px 0 0 rgba($highlight-color, 0.8); + color: inherit; + } + } + + &--content { + display: block; + float: right; + width: 70%; + position: relative; + padding: $padding/3 0 $padding/3 $padding/1.5; + cursor: pointer; + + &:before { + content: ''; + position: absolute; + display: block; + top: 0; + height: 100%; + width: 1px; + background: #ddd; + left: -1px; + } + } + + &--category-header { + position: relative; + border-bottom: 1px solid #ddd; + display: none; + margin-top: $padding/2; + padding: $padding/4 0; + font-size: $header-size; + color: $header-color; + } + + &--wrapper { + width: 100%; + float: left; + padding: $padding/2 0 0; + } + + &--subcategory-column { + float: left; + width: 30%; + padding-left: 0; + text-align: right; + position: relative; + padding: $padding/3 $padding/1.5; + color: $subtitle-color; + font-size: $subtitle-size; + word-wrap: break-word; + + &:before { + content: ''; + position: absolute; + display: block; + top: 0; + height: 100%; + width: 1px; + background: #ddd; + right: 0; + } + + .algolia-docsearch-suggestion--highlight { + background-color: inherit; + color: inherit; + } + } + + &--subcategory-inline { + display: none; + } + + &--title { + margin-bottom: $padding/4; + color: $title-color; + font-size: $title-size; + font-weight: bold; + } + + &--text { + display: desc($include-desc); + line-height: 1.2em; + font-size: $text-size; + color: $text-color; + } + + &--no-results { + width: 100%; + padding: $padding/2 0; + text-align: center; + font-size: 1.2em; + + &::before { + display: none; + } + } + + code { + padding: 1px 5px; + font-size: 90%; + border: none; + color: #222222; + background-color: $code-background; + border-radius: 3px; + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; + + .algolia-docsearch-suggestion--highlight { + background: none; + } + } + // Rules to display categories and subcategories + &.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header { + display: block; + } + + &.algolia-docsearch-suggestion__secondary { + display: block; + } + @media all and (min-width: #{$responsive-breakpoint}) { + .algolia-docsearch-suggestion--subcategory-column { + display: block; + } + } + @media all and (max-width: #{$responsive-breakpoint}) { + .algolia-docsearch-suggestion--subcategory-column { + display: inline-block; + width: auto; + text-align: left; + float: left; + padding: 0; + font-size: 0.9em; + font-weight: bold; + text-align: left; + + &:before { + display: none; + } + + &:after { + content: "|"; + padding: 0 8px; + } + } + + .algolia-docsearch-suggestion--content { + display: inline-block; + width: auto; + text-align: left; + float: left; + padding: 0; + + &:before { + display: none; + } + } + } + } + //Simple layout (no column) + .suggestion-layout-simple { + &.algolia-docsearch-suggestion { + border-bottom: solid 1px #eee; + padding: $padding/2; + margin: 0; + } + + .algolia-docsearch-suggestion { + &--content { + width: 100%; + padding: 0; + + &::before { + display: none; + } + } + + &--category-header { + margin: 0; + padding: 0; + display: block; + width: 100%; + border: none; + + &-lvl0 { + opacity: 0.6; + font-size: $text-size; + } + + &-lvl1 { + opacity: 0.6; + font-size: $text-size; + + &::before { + background-image: url('data:image/svg+xml;utf8,'); + content: ''; + width: 10px; + height: 10px; + display: inline-block; + } + } + } + + &--wrapper { + width: 100%; + float: left; + margin: 0; + padding: 0; + } + + &--duplicate-content, + &--subcategory-inline { + display: none!important; + } + + &--title { + margin: 0; + color: $main-color; + font-size: $title-size; + font-weight: normal; + + &::before { + content: "#"; + font-weight: bold; + color: $main-color; + display: inline-block; + } + } + + &--text { + margin: $padding/4 0 0; + display: desc($include-desc); + line-height: 1.4em; + padding: $padding/3 $padding/2; + background: #f8f8f8; + font-size: $text-size; + opacity: 0.8; + + .algolia-docsearch-suggestion--highlight { + color: darken($text-color,15%); + font-weight: bold; + box-shadow: none; + } + } + } + } + // powered by + .algolia-docsearch-footer { + width: 110px; + height: 20px; + z-index: 2000; + margin-top: $padding/1.5; + float: right; + font-size: 0; + line-height: 0; + + &--logo { + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: center; + background-size: 100%; + overflow: hidden; + text-indent: -9000px; + padding: 0!important; + width: 100%; + height: 100%; + display: block; + } + } + } +} diff --git a/docs/docs/_sass/components/_error.scss b/docs/docs/_sass/components/_error.scss new file mode 100644 index 00000000..b1b21d7c --- /dev/null +++ b/docs/docs/_sass/components/_error.scss @@ -0,0 +1,85 @@ +// Header error layout +// ----------------------------------------------- +// ----------------------------------------------- +body { + color: $color-primary; + background: $background-color; + background-image: url("../img/lines-header-bg.svg"); + background-repeat: repeat-x; + font-family: $base-font-family; + line-height: $base-line-height; +} + +#masthead-error { + padding: ($base-point-grid * 24) 0 ($base-point-grid * 7) 0; + position: relative; + z-index: 100; + + .header-flex { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + + .brand { + margin-right: $base-point-grid * 3; + + .masthead-brand { + position: relative; + top: -10px; + width: 220px; + } + } + + .header-error-text { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 188px; + + .masthead-code-error { + font-size: 7.2rem; + font-weight: normal; + } + + .masthead-title { + font-size: 2.1rem; + font-weight: normal; + } + } + } + + .error-button { + display: block; + background: none; + border: solid 2px; + padding: ($base-point-grid) 26px; + font-size: 1.2rem; + font-weight: normal; + width: 60%; + text-transform: uppercase; + transition: all $base-duration $base-timing; + + &:hover { + background-color: $color-primary; + color: $white; + border: solid 2px $color-primary; + text-decoration: none; + } + } +} + +@include bp(medium) { + #masthead-error { + .header-flex { + flex-direction: column; + .brand { + margin-right: unset; + } + .header-error-text { + margin-top: $base-point-grid * 4; + align-items: center; + } + } + } +} diff --git a/docs/docs/_sass/components/_media-content.scss b/docs/docs/_sass/components/_media-content.scss new file mode 100644 index 00000000..8c9f0acf --- /dev/null +++ b/docs/docs/_sass/components/_media-content.scss @@ -0,0 +1,91 @@ +// MEDIA-CONTENT +// ----------------------------------------------- +// ----------------------------------------------- + +.doc-content { + font-family: $doc-font-family; + + .video-panel { + display: none; + + h2 { + margin-top: ($base-point-grid * 4); + + .fa { + margin-right: 7px; + font-size: 1.75rem; + color: rgba($brand-primary, 0.8); + } + } + + &.toggled { + display: block; + } + + .bg-video { + background: #fff; + padding: ($base-point-grid * 3); + @include bp(large) { + padding: 0; + } + + .wrapper-video { + max-width: 900px; + margin: 0 auto; + + .video-container { + position: relative; + padding-bottom: 56.25%; + height: 0; + + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + } + } + } +} + +.button-video { + padding: $base-point-grid ($base-point-grid * 4); + position: absolute; + right: ($base-point-grid * 4); + top: ($base-point-grid * 7); + font-size: 1.063rem; + color: #fff; + background: $brand-tertiary; + box-shadow: 0 0 20px rgba($brand-secondary, 0.3); + z-index: 10; + @include bp(large) { + width: $column-12; + position: relative; + display: block; + top: 0; + right: 0; + margin-bottom: 20px; + margin-left: 0; + width: 100%; + } + + &:hover { + text-decoration: none; + background: lighten($brand-tertiary, 5%); + box-shadow: 0 0 20px rgba($brand-secondary, 0.5); + color: #fff; + cursor: pointer; + } + + i { + margin-right: 6px; + font-size: 1.125rem; + } + + &.beta { + background: #765; + } +} diff --git a/docs/docs/_sass/components/_navigation.scss b/docs/docs/_sass/components/_navigation.scss new file mode 100644 index 00000000..9e0e05e4 --- /dev/null +++ b/docs/docs/_sass/components/_navigation.scss @@ -0,0 +1,118 @@ +// NAVIGATION +// ----------------------------------------------- +// ----------------------------------------------- + +#navigation { + position: fixed; + z-index: 30; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: ($base-point-grid * 5) ($base-point-grid * 4); + transition: all $base-duration $base-timing; + + .navigation-brand { + font-weight: $font-medium; + text-transform: uppercase; + letter-spacing: 4px; + @include bp(small) { + letter-spacing: 2px; + } + } + + .navigation-menu { + .navigation-menu-item { + display: inline-block; + text-transform: uppercase; + font-size: 0.875rem; + @include bp(medium) { + text-transform: none; + } + + &.active { + border-bottom: 2px solid $brand-tertiary; + } + + & + .navigation-menu-item { + margin-left: ($base-point-grid * 4); + @include bp(small) { + margin-left: ($base-point-grid * 3.2); + } + } + } + + .dropbtn { + @include links($white, $white, rgba($white, 0.5), rgba($white, 0.5)); + cursor: pointer; + border: none; + background: transparent; + color: $white; + font-size: 0.875rem; + margin-bottom: 10px; + } + + .dropdown { + position: relative; + display: block; + outline: 0; + } + /* Documentation Dropdown Content (Hidden by Default) */ + .dropdown-content { + font-size: 0.800rem; + position: absolute; + min-width: 145px; + overflow: auto; + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); + z-index: 1; + margin-left: -8px; + background: rgba($brand-primary, 0.95); + opacity: 0.5; + transform: rotate3d(1, 0, 0, 90deg); + transition: transform ease 250ms, opacity ease 100ms; + transform-origin: top; + + a { + padding: 12px 9px; + text-decoration: none; + display: block; + } + + a:nth-child(4) { + border-top: solid 1px rgba(255, 255, 255, 0.5); + } + } + /* Show the documentatioin dropdown menu (use JS to add this class + to the .dropdown-content container when the user clicks on + the dropdown button) */ + .show { + transform: rotate3d(1,0,0,0); + opacity: 1; + } + } + + a { + @include links($white, $white, rgba($white, 0.5), rgba($white, 0.5)); + + &:hover { + text-decoration: none; + } + } + + .nav-item-text { + @include bp(small) { + display: none; + } + } + + .nav-item-icon { + display: none; + @include bp(small) { + display: inline-block; + } + } + + &.navigation-scroll { + padding: ($base-point-grid * 2) ($base-point-grid * 4); + } +} diff --git a/docs/docs/_sass/components/_sidebar-menu.scss b/docs/docs/_sass/components/_sidebar-menu.scss new file mode 100644 index 00000000..77a2a488 --- /dev/null +++ b/docs/docs/_sass/components/_sidebar-menu.scss @@ -0,0 +1,136 @@ +// Sidebar menu +// ----------------------------------------------- +// ----------------------------------------------- + +.sidebar-menu { + .sidebar-menu-item { + display: flex; + flex-direction: column; + position: relative; + + &.active { + > a, + a.active { + color: $gray-primary; + } + + & > button { + border-left: 4px solid $meta-color; + } + } + + &.open { + a { + background: $meta-hover; + } + + .sub-menu { + max-height: 1600px; + } + + & > button { + background: $meta-hover; + .fa { + @include rotate(90deg); + } + } + } + + button, + a { + display: flex; + justify-content: space-between; + padding: 12px 32px; + width: 100%; + font-size: 1.1rem; + font-family: $base-font-family; + color: $white; + transition: background $base-duration $base-timing; + + .fa { + margin-left: ($base-point-grid * 2); + font-size: 18px; + transition: all .3s; + -moz-osx-font-smoothing: unset; + pointer-events: none; + } + } + + a { + @include links($white, $white, rgba($white, 0.5), rgba($white, 0.5)); + + &:hover { + text-decoration: none; + } + } + + .sub-menu { + max-height: 0px; + overflow: hidden; + transition: max-height 0.8s ease-in-out; + + .sidebar-menu-item { + &.active { + color: $gray-primary; + } + } + + a { + display: block; + align-items: flex-start; + font-size: 1rem; + padding: $base-point-grid ($base-point-grid * 2); + @include links($white, $white, rgba($white, 0.5), rgba($white, 0.5)); + + &:hover { + text-decoration: none; + } + + &:before { + content: "•"; + margin-right: ($base-point-grid * 2); + } + } + } + } +} + +// Sidebar doc versions +// ----------------------------------------------- +// ----------------------------------------------- + +.doc-version-container { + margin-top: $base-point-grid * 7; + border-top: 1px solid rgba($white, 0.1); +} + +.sidebar-doc-versions { + display: flex; + justify-content: center; + + .sidebar-nav-item { + width: 100%; + &.active { + > a { + border-color: transparent; + } + } + ul { + position: sticky; + width: 100%; + z-index: 1; + box-shadow: rgba(0, 0, 0, 0.3) 0px 8px 16px 0px; + background: $brand-primary; + + li { + a { + font-size: 0.800rem; + } + } + + li:nth-child(4) { + border-top: solid 1px rgba(255, 255, 255, 0.5); + } + } + } +} diff --git a/docs/docs/_sass/components/_sidebar-meta.scss b/docs/docs/_sass/components/_sidebar-meta.scss new file mode 100644 index 00000000..a2a24584 --- /dev/null +++ b/docs/docs/_sass/components/_sidebar-meta.scss @@ -0,0 +1,81 @@ +.meta { + background: $meta-background; +} + +#cat-meta-dropdown { + min-width: $sidebar-cat-button; + + .sidebar-nav { + .sidebar-nav-item { + > a { + transition: background $base-duration $base-timing; + &:hover { + background: $meta-hover; + box-shadow: $sidebar-cat-dropdow; + } + } + + &.active { + > a { + background: $meta-hover; + box-shadow: $sidebar-cat-dropdow; + } + } + + ul { + width: 100%; + z-index: 1; + box-shadow: $sidebar-cat-dropdow; + background: $meta-hover; + + a { + color: $white; + transition: all $base-duration $base-timing; + + &:hover { + background: rgba($white, 0.05); + color: rgba($white, 0.8); + } + } + } + } + } +} + +#doc-meta-version-button { + .sidebar-doc-versions { + .sidebar-nav-item { + background: $meta-background; + > a { + transition: background $base-duration $base-timing; + &:hover { + background: $meta-hover; + box-shadow: $sidebar-cat-dropdow; + } + } + + &.active { + > a { + background: $meta-hover; + box-shadow: $sidebar-cat-dropdow; + } + } + + ul { + width: 100%; + z-index: 1; + box-shadow: $sidebar-cat-dropdow; + background: $meta-hover; + + a { + color: $white; + padding-left: 10px; + + &:hover { + @include links($white, $white, rgba($white, 0.5), rgba($white, 0.5)); + } + } + } + } + } +} diff --git a/docs/docs/_sass/components/_sidebar.scss b/docs/docs/_sass/components/_sidebar.scss new file mode 100644 index 00000000..05342790 --- /dev/null +++ b/docs/docs/_sass/components/_sidebar.scss @@ -0,0 +1,174 @@ +// SIDEBAR +// ----------------------------------------------- +// ----------------------------------------------- + +#wrapper { + padding-left: $sidebar-width; + transition: padding 0.4s ease-in-out; + + &.toggled { + padding-left: 0; + } +} + +#sidebar-wrapper { + left: 0; + height: 100%; + overflow-y: auto; + position: fixed; + transition: left 0.4s ease-in-out; + width: $sidebar-width; + z-index: 11; + + .sidebar-toggle { + display: none; + } +} + +#wrapper.toggled { + #sidebar-wrapper { + left: -$sidebar-width; + } +} + +.toggle-container { + display: flex; + justify-content: end; + margin-top: ($base-point-grid * 1); +} + +.sidebar-toggle { + background: none; + border: none; + color: $brand-tertiary; + padding: 28px 32px; + position: relative; + text-align: center; + text-decoration: none; + transition: color 0.3s ease, transform 0.3s ease; + + .menu-icon { + position: absolute; + top: 37%; + left: 38%; + } + + &:hover { + color: $brand-primary; + cursor: pointer; + transform: scaleX(1.5); + } +} + +.sidebar-brand { + padding: 60px 0 30px; + display: flex; + flex-direction: column; + align-items: center; + + .brand-title { + font-family: $base-font-family; + font-size: 18px; + font-weight: $font-medium; + text-transform: uppercase; + letter-spacing: 1px; + line-height: 23px; + color: $white; + } +} + +.sidebar-nav { + font-size: $base-docs-font-size; + + .sidebar-nav-item { + > a { + display: flex; + align-items: center; + justify-content: space-between; + @include links($white, $white, $white, $white); + padding: ($base-point-grid * 1.5) ($base-point-grid * 4); + + &:hover { + text-decoration: none; + + } + + .fa { + margin-left: ($base-point-grid * 2); + font-size: 18px; + transition: all .3s; + -moz-osx-font-smoothing: unset; + } + } + + &.active { + > a { + padding-left: 28px; + .fa { + @include rotate(90deg); + } + } + } + + + ul { + display: none; + + + li { + a { + display: flex; + align-items: flex-start; + @include links($white, $white, rgba($white, 0.5), rgba($white, 0.5)); + padding: $base-point-grid ($base-point-grid * 2); + font-size: 15px; + + + &.active { + color: $gray-primary; + } + + &:hover { + text-decoration: none; + } + + } + } + } + } +} + +@include bp(medium) { + #wrapper { + padding-left: 0; + transition: all 0.4s ease-in-out; + } + + #sidebar-wrapper { + left: -100%; + } + + #wrapper.toggled { + #sidebar-wrapper { + left: 0; + top: 0; + width: 100%; + + .sidebar-toggle { + display: block; + opacity: 0.7; + transition: opacity 0.3s ease, transform 0.3s ease; + + &:hover { + opacity: 1; + transform: rotate(-180deg); + } + } + } + + #doc-wrapper { + height: 100vh; + overflow: hidden; + } + } +} diff --git a/docs/docs/_sass/utils/_variables.scss b/docs/docs/_sass/utils/_variables.scss new file mode 100644 index 00000000..079ea0a8 --- /dev/null +++ b/docs/docs/_sass/utils/_variables.scss @@ -0,0 +1,117 @@ +// Variables +// ----------------------------------------------- +// ----------------------------------------------- +// Colors +// ----------------------------------------------- +$brand-primary: #263238; +$brand-secondary: #DCDEE0; +$brand-tertiary: #C5C8CB; +$gray-primary: #A9AAAB; +$white: rgb(255, 255, 255); +$link-color: darken(#263238, 10%); +$link-hover: darken(#263238, 15%); +$background-color: #F5F7F8; +$color-primary: #263238; +$background-color: #F5F7F8; +$box-text-color: #263238; +$dark-color-doc: #273244; +// meta +$meta-color: #CD151D; +$meta-background: #2E3B44; +$meta-hover: #424E56; +// Typography +// ----------------------------------------------- +$base-font-family: 'Iosevka Web'; +$header-font-family: $base-font-family; +$code-font-family: $base-font-family; +$doc-font-family: 'Open Sans', sans-serif; +$base-font-color: $brand-primary; +$header-font-color: $brand-primary; +$font-regular: 400; +$font-medium: 500; +$font-semibold: 600; +$font-bold: 700; +$base-font-size: 14px; +$base-line-height: 28px; +$base-docs-font-size: 16px; +$heading-font-family: 'Montserrat', sans-serif; +// Sizes +// ----------------------------------------------- +$base-point-grid: 8px; +$sidebar-cat-button: 164px; +$sidebar-doc-version-button: 200px; +$height-home-code-block: 400px; +$width-home-code-block: 580px; +$github-modal-width: 178px; +$text-box-icon-size: 27px; +// Animation +// ----------------------------------------------- +$short-duration: 150ms; +$base-duration: 350ms; +$doble-base-duration: 500ms; +$base-timing: ease-in-out; +$opacity-transition: opacity $base-duration $base-timing, transform $base-duration $base-timing; +$long-transition: opacity $doble-base-duration $base-timing; +$icons-transition: opacity 400ms ease; +// Breakpoint +// ----------------------------------------------- +$bp-small: 480px; +$bp-medium: 768px; +$bp-large: 992px; +$bp-xlarge: 1140px; +// Grid +// ----------------------------------------------- +$column-1: (1/12*100%); +$column-2: (2/12*100%); +$column-3: (3/12*100%); +$column-4: (4/12*100%); +$column-5: (5/12*100%); +$column-6: (6/12*100%); +$column-7: (7/12*100%); +$column-8: (8/12*100%); +$column-9: (9/12*100%); +$column-10: (10/12*100%); +$column-11: (11/12*100%); +$column-12: (12/12*100%); +$gutter-margin: ($base-point-grid * 4); +// Border +// ----------------------------------------------- +$border-color: rgba($gray-primary, 0.1); +$doc-header-border: rgba($brand-secondary, 0.8); +$sidebar-cat-dropdow: rgba(0, 0, 0, 0.2) 0 2px 8px 0; +// Code +// ----------------------------------------------- +$code-border-color: rgba($gray-primary, 0.2); +$code-padding: 16px; +$code-margin: 30px; +// Sidebar Sizes +// ----------------------------------------------- +$sidebar-width: 325px; +// Box Shadow +$box-shadow-item: rgba($brand-primary, 0.10) 0 0 20px; +$box-shadow-button: rgba(0,0,0,0.3) 0 2px 8px 0; +$base-box-shadow: 0 4px 10px rgba($meta-background, 0.08); +// Docsearch dropdown +$dropdown-config: ( + main-color: $brand-tertiary, + layout-type: normal, + layout-width: full, + background-color: #fff, + border-radius: 2, + border-width: 2, + border-color: rgba(0, 0, 0, .1), + box-shadow: none, + branding-position: bottom, + spacing: normal, + include-desc: yes, + background-category-header: #ffffff, + font-size: normal, + header-color: $brand-primary, + title-color: $brand-primary, + subtitle-color: $base-font-color, + text-color: $brand-secondary, + highlight-color: #ef8836, + highlight-opacity: 0.3, + highlight-type: underline +) +!default; diff --git a/docs/docs/_sass/utils/mixins/_breakpoint.scss b/docs/docs/_sass/utils/mixins/_breakpoint.scss new file mode 100644 index 00000000..4882afa8 --- /dev/null +++ b/docs/docs/_sass/utils/mixins/_breakpoint.scss @@ -0,0 +1,25 @@ +// Breakpoint +// ----------------------------------------------- +// ----------------------------------------------- +@mixin bp($point) { + @if $point==xlarge { + @media (max-width: $bp-xlarge) { + @content; + } + } + @if $point==large { + @media (max-width: $bp-large) { + @content; + } + } + @if $point==medium { + @media (max-width: $bp-medium) { + @content; + } + } + @if $point==small { + @media (max-width: $bp-small) { + @content; + } + } +} diff --git a/docs/docs/_sass/utils/mixins/_general-mixins.scss b/docs/docs/_sass/utils/mixins/_general-mixins.scss new file mode 100644 index 00000000..d9b2cd73 --- /dev/null +++ b/docs/docs/_sass/utils/mixins/_general-mixins.scss @@ -0,0 +1,9 @@ +// Transform +//------------------------------------------------ +@mixin rotate($degrees) { + -webkit-transform: rotate($degrees); + -moz-transform: rotate($degrees); + -ms-transform: rotate($degrees); + -o-transform: rotate($degrees); + transform: rotate($degrees); +} diff --git a/docs/docs/_sass/utils/mixins/_links.scss b/docs/docs/_sass/utils/mixins/_links.scss new file mode 100644 index 00000000..4e3c354c --- /dev/null +++ b/docs/docs/_sass/utils/mixins/_links.scss @@ -0,0 +1,20 @@ +// Links +//------------------------------------------------ +@mixin links ($link, $visited, $hover, $active) { + & { + color: $link; + + &:visited { + color: $visited; + } + + &:hover { + color: $hover; + } + + &:active, + &:focus { + color: $active; + } + } +} diff --git a/docs/docs/conditions/README.md b/docs/docs/conditions/README.md new file mode 100644 index 00000000..b6ff60f5 --- /dev/null +++ b/docs/docs/conditions/README.md @@ -0,0 +1,125 @@ +--- +layout: docs-analysis +title: Analysis - Pre and post-conditions +permalink: /conditions/ +--- + +# Pre and post-conditions + +In the Quick Start we've introduced the idea of pre and post-conditions of functions as promises that either the caller or the body of the function should obbey. Here we go deeper in the topic, about how these conditions may look, how they compose, and which way they are checked. + +## Pre-conditions + +When using Λrrow Analysis, each function may declare one or more _pre-conditions_, which describe what should be true of the arguments of each call to it. In other words, the **caller** of the function must ensure that pre-conditions are true on every single call. + +```kotlin +import arrow.analysis.pre + +fun increment(x: Int): Int { + pre(x > 0) { "value must be positive" } + return x + 1 +} +``` + +Each pre-condition takes a Boolean condition and a block with a message describing such condition. Even though the Kotlin compiler allows any expression to appear in those positions, there are heavy restrictions on what is actually understood by Λrrow Analysis. + +- Pre-conditions may only talk about parameters to the function, including `this` when inside a class method or defining an extension function; +- You can only create Boolean expressions using basic arithmetic operations (addition, subtraction, ...), comparisons, and simple Boolean operations (and, or, not). In particular, you cannot define a Boolean function and use it in a condition; +- You may use `if` or `when`, but in the latter case only _without_ a subject; +- The final block must be a simple constant string. We follow this pattern for compatibility with Kotlin's built-in `require` function. + +### Errors related to pre-conditions + +The most common error in Λrrow Analysis is calling a function while not satisfying its pre-conditions. + +```kotlin +val example = increment(-1) +``` +``` +e: pre-condition `value must be positive` is not satisfied in `increment(-1)` + -> unsatisfiable constraint: `(-1 > 0)` +``` + +When more than one block of pre-conditions is present, the tool also checks that those pre-conditions are not inconsistent. Imagine we require for the a number to be both positive and negative: + +```kotlin +import arrow.analysis.pre + +fun wat(x: Int): Int { + pre(x > 0) { "value must be positive" } + pre(x < 0) { "value must be negative" } + return 0 +} +``` +``` +e: `wat` has inconsistent pre-conditions: (x < 0), (x > 0) +``` + +Think about it: there's no value that can satisfy the requirements imposed by `wat`. Such a restriction usually points to an error in the specification of the function, since any usage would be completely forbidden otherwise. + +## Disabling checks + +If you are completely sure that a pre-condition is satisfied, even though Λrrow Analysis is not able to "see" it, you can disable checking. This case usually arises for data which comes from external input. However, we still encourage you to handle the possibility of failure, instead of blindly disabling the checks: an exception may be hiding behind that call... Λrrow Analysis gives three different ways to disable checks, depending on the desired level of granularity. + +- `unsafeCall` disables checks only for the call *directly nested* within it. If you need to disable a check, this is the recommended choice, because it removes the fewer guarantees. + + ```kotlin + import arrow.analysis.unsafeCall + + val wrong1 = unsafeCall(increment(-2)) // no errors reported + val wrong2 = unsafeCall(increment(increment(-2))) + // ^ ^ + // disabled not disabled + // error: pre-condition is not satisfied in `increment(-2)` + ``` + +- `unsafeBlock` disables checks for everything inside that block. If you want to silence all errors in a function or value declaration, wrap its entire body with `unsafeBlock`. + + ```kotlin + import arrow.analysis.unsafeBlock + + val wrong3 = unsafeBlock { increment(increment(-2)) } // no errors reported + ``` +- `@Suppress` can be attached to a declaration to silence an entire type of errors or warnings in their body. For example, `"UnsatCallPre"` are those errors coming from **unsat**isfied **pre**conditions on **call**s. + + ```kotlin + @Suppress("UnsatCallPre") + val wrong4: Int = increment(emptyList().first()) + ``` + +## Post-conditions + +Post-conditions describe the **promises** we give about the result of the function. They are quite important when composing larger programs, because post-conditions define which information about the inner computation we want to expose about to the rest of the program. For example, these two programs are valid: + +```kotlin +import arrow.analysis.post + +fun one() = 1.post({ it == 1 }) { "result is exactly 1" } +fun positive() = 1.post({ it > 0 }) { "result is positive" } +``` + +but in the latter case we only promise that the result is positive. This leaves us room for changing the code later on, while keeping the rest of the code which depended on that condition untouched. + +Post-conditions are attached to the result value by calling the `post` extension function. The restrictions for the arguments are similar to those of pre-conditions: + +- The first argument should be a **block** whose body talks only about the parameters of the function and the formal parameter of the block, which represents the return value. +- The Boolean expression and the message follow the same restrictions as above. + +### Errors related to post-conditions + +The most common error message related to post-conditions is for them not being true in the _general_ case. This means that there's at least one set of values for the parameters for which the post-condition is not true. Take the following example: + +```kotlin +import arrow.analysis.pre +import arrow.analysis.post + +fun nope(x: Int): Int { + pre(x >= 0) { "value >= 0" } + return (x + 1).post({ it > 1 }) { "result > 1" } +} +``` +``` +e: declaration `nope` fails to satisfy the post-condition: ($result > 1) +``` + +In this case, when `x` is `0`, the result value is `1`, which is not stricly greater than `1`. Unfortunately, the error messages produced in this case by Λrrow Analysis are not always very useful to diagnose where the problem lies; we are currently working on improving this feature. \ No newline at end of file diff --git a/docs/docs/control/README.md b/docs/docs/control/README.md new file mode 100644 index 00000000..9f622e4e --- /dev/null +++ b/docs/docs/control/README.md @@ -0,0 +1,124 @@ +--- +layout: docs-analysis +title: Analysis - Control operators +permalink: /control/ +--- + +# Control operators + +When dealing with pre-conditions, the _environment_ in which a call takes place is very important. You introduce new information in the environment every time you use a control operator like `if` or `when`. For example, the following is accepted by Λrrow Analysis, since you are manually checking whether the `size` of the list if enough for `get`in the right value: + +```kotlin +fun List.firstOr(default: A) = + if (this.size > 0) this.get(0) else default +``` + +If your check is not strong enough (or wrong altogether), the error message provides additional information about what is known in that branch. For example, suppose you've accidentally switched the order of the branches above: + +```kotlin +fun List.firstOr(default: A): A = + if (this.size > 0) default else this.get(0) +``` +``` +e: Example.kt: (2, 35): pre-condition `index within bounds` is not satisfied in `get(0)` + -> unsatisfiable constraint: `((0 >= 0) && (0 < this.size))` + -> in branch: ( ! this.size > 0), cond17 +``` + +This tells you that at that point you know that the `size` is not greater than 0 (note the `!` at the beginning of the expression). Unfortunately, sometimes "garbage" formulae (such as `cond17`) above appear in the output. We are working on ways to prune these useless constraints, in the time being just ignore them. + +The environment is also important when checking post-conditions. Whenever you have some branching operation, like `if` or `when`, the post-condition is checked on **each** branch **separately**, even when the `post` appears as part of a common expression. This allows Λrrow Analysis to accept the following, in which whether the result is non-negative depends on the prior check over `n`. + +```kotlin +import arrow.analysis.post + +fun absoluteValue(n: Int): Int = when { + n < 0 -> -n + n == 0 -> 0 + else -> n +}.post({ it >= 0 }) { "result >= 0" } +``` + +## Unreachable code + +Being aware of the environment makes Λrrow Analysis able to detect some cases of unreachable code. Here's a simple (but not very useful) example, in which we can guarantee that `1` is never returned because the case `x < 0` cannot arise thanks to the pre-condition. + +```kotlin +import arrow.analysis.pre + +fun boo(x: Int): Int { + pre(x > 0) { "x must be positive" } + return if (x < 0) 1 else 2 +} +``` +``` +e: unreachable code due to conflicting conditions: x < 0, (x == x), (0 == 0), (x < 0 == (x < 0)), (x > 0) + -> main function body +``` + +This is another case in which we continue working on pruning useless information from the error messages. But you can see that `x < 0` and `x > 0` appear in the list, and those two expressions together are inconsistent -- that is, there's no way for a value to satisfy both. + +Λrrow Analysis detects unreachable code in a best-effort basis. Conflicts may arise not only between pre-conditions and conditionals, but also between several conditionals, and even between the post-conditions of a function and its environment. + +## No higher-order support + +Λrrow Analysis is not able to propagate information via higher-order functions. For example, if you `map` the `absoluteValue` function defined above over a list of numbers, the knowledge that *each* element of the list is non-negative is not represented within the system. The following is rejected, for example: + +```kotlin +import arrow.analysis.post + +val okButRejected = listOf(-1).map { absoluteValue(it) }.first() + .post({ it >= 0 }) { "result is non-negative" } +``` + +This does not mean that the tool is useless on such operations. In this case, we can still express the fact that the size is maintained by `map`, since that condition does *not* depend on the transformation function. As a result, the following is accepted, since Λrrow Analysis tracks the length of the list through `map`, and can see that the call to `first()` is correct. + +```kotlin +val ok = listOf(-1).map { absoluteValue(it) }.first() +``` + +### Scope functions + +There's a handful of higher-order functions which play a significant role in Kotlin programs, the so-called [scope functions](https://kotlinlang.org/docs/scope-functions.html) `let`, `run`, `with`, `apply`, and `also`. Given its importance, Λrrow Analysis ships with special support for them: when used with a lambda as second argument, the tool can "look inside" the body during the checks. + +This means you can choose whatever code style suits you best. Λrrow Analysis can handle local variables, + +```kotlin +import arrow.analysis.pre +import arrow.analysis.post + +fun double(n: Int): Int { + pre(n > 0) { "n positive" } + val z = n + n + val r = z + 1 + return r.post({ it > 0 }) { "result positive" } +} +``` + +as well as chains of scope functions, + +```kotlin +import arrow.analysis.pre +import arrow.analysis.post + +fun double2(n: Int): Int { + pre(n > 0) { "n positive" } + return (n + n).let { it + 1 } + .post({ it > 0 }) { "result positive" } +} +``` + +## Null safety + +Λrrow Analysis can reason about `null` values, in a similar way as the [Kotlin compiler does](https://kotlinlang.org/docs/null-safety.html). The Elvis safe call operator `?.` is recognized, and in combination with the aforementioned support for scope functions, the tool can handle idiomatic code such as the following. + +```kotlin +import arrow.analysis.pre +import arrow.analysis.post + +fun incrementNotNull(x: Int?): Int? { + pre((x == null) || (x > 0)) { "x is null or positive" } + val y = x?.let { it + 1 } + return y.post({ (it == null) || (it > 1) }) { "null or greater than 1" } +} +``` \ No newline at end of file diff --git a/docs/docs/css/styles.scss b/docs/docs/css/styles.scss new file mode 100644 index 00000000..804cde4b --- /dev/null +++ b/docs/docs/css/styles.scss @@ -0,0 +1,30 @@ +--- +--- +// Utils +@import "utils/variables"; +@import "utils/mixins/links"; +@import "utils/mixins/breakpoint"; +@import "utils/mixins/general-mixins"; + +// Base +@import "base/reset"; +@import "base/base"; +@import "base/helpers"; +@import "base/fonts"; + +// Components +@import "components/code"; +@import "components/button"; +@import "components/dropdown"; +@import "components/doc-body"; +@import "components/doc-content-meta"; +@import "components/doc-content"; +@import "components/doc-header"; +@import "components/media-content"; +@import "components/sidebar-meta"; +@import "components/sidebar-menu"; +@import "components/sidebar"; +@import "components/navigation"; +@import "components/error"; + +@include dropdown($dropdown-config...); diff --git a/docs/docs/error.md b/docs/docs/error.md new file mode 100644 index 00000000..10ad7a77 --- /dev/null +++ b/docs/docs/error.md @@ -0,0 +1,3 @@ +--- +layout: error +--- diff --git a/docs/docs/fields/README.md b/docs/docs/fields/README.md new file mode 100644 index 00000000..0e2d6634 --- /dev/null +++ b/docs/docs/fields/README.md @@ -0,0 +1,62 @@ +--- +layout: docs-analysis +title: Analysis - Fields +permalink: /fields/ +--- + +# Fields + +The declaration of pre and post-conditions may not only talk about the value of the arguments, but also reference their properties, fields, and even some of their functions. This is used, for example, in the contract of the indexing operation of a list, in which we refer to its `size`. + +```kotlin +import arrow.analysis.pre + +class List { + val size: Int + get() = TODO() // complicated computation + + fun get(index: Int): T { + pre(index >= 0 && index < this.size) { "index within bounds" } + // complicated code to get the value + } +} +``` + +We use the word **field** to collectively refer to those elements of an argument we are allowed to refer to in a pre- or postcondition, or an invariant of a mutable variable or type. There are two sources for fields: + +1. Properties and fields, like `size` above. +2. Instance or extension methods with _no_ arguments, this allows you to use `isNotEmpty()` as a field. + +Given the rules above, the following is accepted by Λrrow Analysis: + +```kotlin +import arrow.analysis.pre + +fun List.first(): T { + pre(this.isNotEmpty()) { "list should not be empty" } + return this.get(0) +} +``` + +## Definition of fields + +Actually, if you think about it, the fact that the previous code snippet is accepted is not obvious at all! There must be an additional reasoning step for Λrrow Analysis to understand that is the list is not empty, then calling `get` with `0` as index is allowed, since the precondition for `get` only mentions `size`. + +It is very common, though, to have this kind of relationship between properties. Furthermore, many style guidelines suggest to use simpler Boolean predicates like `isNotEmpty()` instead of the longer `size > 0`. To establish this broken link, Λrrow Analysis follows this rule: + +> If a field declares **no** preconditions, and a **single** postcondition of the form `{ it == SOMETHING }`, then `SOMETHING` is taken as the **definition** of that field. + +The tool then deems each usage of the derived field as being equivalent to its definition. In our case, the `List` class would declare the postcondition in `isNotEmpty`. + +```kotlin +import arrow.analysis.post + +class List { + fun isNotEmpty(): Boolean { + // complicated code + return something.post({ this.size > 0 }) { "non-emptiness is size > 0" } + } +} +``` + +We remark that this definition only applies at the level of Λrrow Analysis. The _implementation_ of `isNotEmpty` is free to use a more performant algorithm. It's during the reasoning stage within the analysis that we make use of the equivalence with `size > 0`. \ No newline at end of file diff --git a/docs/docs/fonts/iosevka-bold.ttf b/docs/docs/fonts/iosevka-bold.ttf new file mode 100644 index 00000000..f49579f5 Binary files /dev/null and b/docs/docs/fonts/iosevka-bold.ttf differ diff --git a/docs/docs/fonts/iosevka-medium.ttf b/docs/docs/fonts/iosevka-medium.ttf new file mode 100644 index 00000000..0d186f1b Binary files /dev/null and b/docs/docs/fonts/iosevka-medium.ttf differ diff --git a/docs/docs/fonts/iosevka-regular.ttf b/docs/docs/fonts/iosevka-regular.ttf new file mode 100644 index 00000000..2c08b80f Binary files /dev/null and b/docs/docs/fonts/iosevka-regular.ttf differ diff --git a/docs/docs/fonts/iosevka-semibold.ttf b/docs/docs/fonts/iosevka-semibold.ttf new file mode 100644 index 00000000..bf476c75 Binary files /dev/null and b/docs/docs/fonts/iosevka-semibold.ttf differ diff --git a/docs/docs/fonts/ttf/iosevka-bold.ttf b/docs/docs/fonts/ttf/iosevka-bold.ttf new file mode 100644 index 00000000..f49579f5 Binary files /dev/null and b/docs/docs/fonts/ttf/iosevka-bold.ttf differ diff --git a/docs/docs/fonts/ttf/iosevka-medium.ttf b/docs/docs/fonts/ttf/iosevka-medium.ttf new file mode 100644 index 00000000..0d186f1b Binary files /dev/null and b/docs/docs/fonts/ttf/iosevka-medium.ttf differ diff --git a/docs/docs/fonts/ttf/iosevka-regular.ttf b/docs/docs/fonts/ttf/iosevka-regular.ttf new file mode 100644 index 00000000..2c08b80f Binary files /dev/null and b/docs/docs/fonts/ttf/iosevka-regular.ttf differ diff --git a/docs/docs/fonts/ttf/iosevka-semibold.ttf b/docs/docs/fonts/ttf/iosevka-semibold.ttf new file mode 100644 index 00000000..bf476c75 Binary files /dev/null and b/docs/docs/fonts/ttf/iosevka-semibold.ttf differ diff --git a/docs/docs/fonts/woff/iosevka-bold.woff b/docs/docs/fonts/woff/iosevka-bold.woff new file mode 100644 index 00000000..09e8050f Binary files /dev/null and b/docs/docs/fonts/woff/iosevka-bold.woff differ diff --git a/docs/docs/fonts/woff/iosevka-medium.woff b/docs/docs/fonts/woff/iosevka-medium.woff new file mode 100644 index 00000000..66127228 Binary files /dev/null and b/docs/docs/fonts/woff/iosevka-medium.woff differ diff --git a/docs/docs/fonts/woff/iosevka-regular.woff b/docs/docs/fonts/woff/iosevka-regular.woff new file mode 100644 index 00000000..0888ba57 Binary files /dev/null and b/docs/docs/fonts/woff/iosevka-regular.woff differ diff --git a/docs/docs/fonts/woff/iosevka-semibold.woff b/docs/docs/fonts/woff/iosevka-semibold.woff new file mode 100644 index 00000000..65bfa079 Binary files /dev/null and b/docs/docs/fonts/woff/iosevka-semibold.woff differ diff --git a/docs/docs/fonts/woff2/iosevka-bold.woff2 b/docs/docs/fonts/woff2/iosevka-bold.woff2 new file mode 100644 index 00000000..99f08203 Binary files /dev/null and b/docs/docs/fonts/woff2/iosevka-bold.woff2 differ diff --git a/docs/docs/fonts/woff2/iosevka-medium.woff2 b/docs/docs/fonts/woff2/iosevka-medium.woff2 new file mode 100644 index 00000000..10d57a03 Binary files /dev/null and b/docs/docs/fonts/woff2/iosevka-medium.woff2 differ diff --git a/docs/docs/fonts/woff2/iosevka-regular.woff2 b/docs/docs/fonts/woff2/iosevka-regular.woff2 new file mode 100644 index 00000000..b6a059fd Binary files /dev/null and b/docs/docs/fonts/woff2/iosevka-regular.woff2 differ diff --git a/docs/docs/fonts/woff2/iosevka-semibold.woff2 b/docs/docs/fonts/woff2/iosevka-semibold.woff2 new file mode 100644 index 00000000..4394cd57 Binary files /dev/null and b/docs/docs/fonts/woff2/iosevka-semibold.woff2 differ diff --git a/docs/docs/img/analysis.svg b/docs/docs/img/analysis.svg new file mode 100644 index 00000000..9e249a01 --- /dev/null +++ b/docs/docs/img/analysis.svg @@ -0,0 +1,61 @@ + + + Analysis@1x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/docs/img/arrow-brand-error.svg b/docs/docs/img/arrow-brand-error.svg new file mode 100644 index 00000000..d29d7cdc --- /dev/null +++ b/docs/docs/img/arrow-brand-error.svg @@ -0,0 +1,19 @@ + + + + arrow-brand-error + Created with Sketch. + + + + + + + + + + + + + + diff --git a/docs/docs/img/favicon.png b/docs/docs/img/favicon.png new file mode 100644 index 00000000..dac4503b Binary files /dev/null and b/docs/docs/img/favicon.png differ diff --git a/docs/docs/img/lines-header-bg.svg b/docs/docs/img/lines-header-bg.svg new file mode 100644 index 00000000..23b510f7 --- /dev/null +++ b/docs/docs/img/lines-header-bg.svg @@ -0,0 +1,98 @@ + + + + 8E2DD1B1-380D-4EB8-B06B-4346B7AE3E5F + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/docs/img/poster.png b/docs/docs/img/poster.png new file mode 100644 index 00000000..3af08757 Binary files /dev/null and b/docs/docs/img/poster.png differ diff --git a/docs/docs/img/search-icon.svg b/docs/docs/img/search-icon.svg new file mode 100644 index 00000000..8ef855ba --- /dev/null +++ b/docs/docs/img/search-icon.svg @@ -0,0 +1,16 @@ + + + + icon + Created with Sketch. + + + + + + + + + + + diff --git a/docs/docs/index.md b/docs/docs/index.md new file mode 100644 index 00000000..6fdc28a3 --- /dev/null +++ b/docs/docs/index.md @@ -0,0 +1,202 @@ +--- +layout: docs-analysis +title: Analysis - Quick Start +permalink: / +--- + +[![Latest snapshot](https://img.shields.io/maven-metadata/v?color=0576b6&label=latest%20snapshot&metadataUrl=https%3A%2F%2Foss.sonatype.org%2Fservice%2Flocal%2Frepositories%2Fsnapshots%2Fcontent%2Fio%2Farrow-kt%2Farrow-analysis-common%2Fmaven-metadata.xml)](https://oss.sonatype.org/service/local/repositories/snapshots/content/io/arrow-kt/arrow-analysis-common/) +[![Publish artifacts](https://github.com/arrow-kt/arrow-meta/workflows/Publish%20Artifacts/badge.svg)](https://github.com/arrow-kt/arrow-meta/actions?query=workflow%3A%22Publish+Artifacts%22) +[![Publish documentation](https://github.com/arrow-kt/arrow-meta/workflows/Publish%20Documentation/badge.svg)](https://github.com/arrow-kt/arrow-meta/actions?query=workflow%3A%22Publish+Documentation%22) +[![Kotlin version badge](https://img.shields.io/badge/kotlin-1.6-blue.svg)](https://kotlinlang.org/docs/whatsnew16.html) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) + +# Λrrow Analysis. Beyond the Compiler + +Λrrow Analysis introduces new checks in your compilation pipeline, which warn about common mistakes like out of bounds indexing. This Quick Start explains how to set up Λrrow Analysis in your Gradle project, and how to use it to get further insight in your code, and to introduce additional checks in your own functions and classes. + +In this Quick Start we assume a Kotlin project, Λrrow Analysis also provides preliminary [support for Java]({{ '/java' | relative_url }}). + +> Λrrow Analysis is built on top of the [Λrrow Meta]({{ 'http://arrow-kt.io/docs/meta' | relative_url }}) meta-programming library for the Kotlin compiler. + +## Adding the plug-in + +Open your Gradle build file, and add the following lines: + +
+ +
+ + +
+ +
+ +```kotlin +buildscript { + dependencies { + classpath("io.arrow-kt.analysis.kotlin:io.arrow-kt.analysis.kotlin.gradle.plugin:2.0") + } +} + +apply(plugin = "io.arrow-kt.analysis.kotlin") +``` + +
+ +
+ +```groovy +buildscript { + dependencies { + classpath 'io.arrow-kt.analysis.kotlin:io.arrow-kt.analysis.kotlin.gradle.plugin:2.0' + } +} + +apply plugin: 'io.arrow-kt.analysis.kotlin' +``` + +
+
+ +This adds both the Kotlin compiler plug-in -- which performs the checks -- and the pre and post-conditions for the Kotlin standard library. You are ready to get your first analysis results. + +### Λrrow Analysis + Android + +If you want to use the plug-in in an Android project, you may run into [this Kotlin compiler issue](https://youtrack.jetbrains.com/issue/KT-38576), characterized by the following error message: + +``` +java.lang.AssertionError: Duplicated JavaClassDescriptor ... reported to IC +``` + +To solve this problem you have to disable precise Java tracking, by adding the following line in your `gradle.properties` file: + +```kotlin +kotlin.incremental.usePreciseJavaTracking=false +``` + +## Running the analysis + +Open a new file and write the following line. This code is incorrect because you want to obtain the third element of an empty list. + +```kotlin +val wrong = emptyList().get(2) +``` + +Run the analysis by executing the corresponding Gradle task (usually `build` or `compileKotlin`), and (if everything is correctly configured) you should get the following message: + +``` +e: Example.kt: (1, 18): pre-condition `index within bounds` is not satisfied in `get(2)` + -> unsatisfiable constraint: `((2 >= 0) && (2 < emptyList().size))` + -> `2` bound to param `index` in `kotlin.collections.List.get` + -> main function body +``` + +There's a lot of information there, so let's break it into pieces: + +1. `Example.kt: (1, 18)`: the place where the problem lies (but you already knew that 😜); +2. `pre-condition 'index within bounds' is not satisfied`: this is the description of the problem. Something which should be true ("index within bounds") for the arguments of a function (_pre-condition_) is not true (not _satisfied_); +3. `((2 >= 0) && (2 < emptyList().size))`: this is the formula which expresses the "index within bounds" pre-condition more formally. By inspecting this formula, you can see that the first half (`2 >= 0`) is OK, but there are problems with the second half (`2 < emptyList().size`), since that size is 0; +4. `'2' bound to param 'index'`: this is additional information about the function call; +5. `main function body`: the last part of the message describes branching information. For example, if we had an `if` expression, it would tell us whether we are in the "condition true" branch of the "condition false" branch. When there are no conditions, we just speak of _main function body_. + +Errors arising from function calls whose pre-conditions are not safisfied are the **main** type of problems you'll encounter in the usage of Λrrow Analysis. + +## Checks in functions + +Λrrow Analysis extends the contract mechanism provided by Kotlin, and attaches two pieces of information to each function: + +- its _pre-conditions_ describe what should be true about the arguments given to a function call, +- its _post-conditions_ describe what is true about the returned value of the function. Note that it only makes sense to talk about post-conditions once we know the pre-conditions hold. + +Let's write a small function which increments an integral value: + +```kotlin +fun increment(x: Int): Int = x + 1 +``` + +However, in our domain it only makes sense to call this function with positive numbers: the perfect job for a pre-condition. Alas, adding this pre-condition forces us to turn the simple function into a block and use `return`. + +```kotlin +import arrow.analysis.pre + +fun increment(x: Int): Int { + pre(x > 0) { "value must be positive" } + return x + 1 +} +``` + +You can check that the pre-condition works by calling the function with a negative number. + +```kotlin +val example = increment(-1) +``` +``` +e: pre-condition `value must be positive` is not satisfied in `increment(-1)` + -> unsatisfiable constraint: `(-1 > 0)` +``` + +### Post-conditions + +But what about if we change the code to the following? + +```kotlin +val example = increment(increment(1)) +``` + +A very similar error arises: + +``` +e: pre-condition `value must be positive` is not satisfied in `increment(increment(1))` +-> unsatisfiable constraint: `(increment(1) > 0)` +``` + +This error tells us that Λrrow Analysis was not able to deduce whether `increment(1)` is positive or not. To fix the problem, we need to introduce a _post-condition_, a **promise** about the result of the function. In this case, we know that given a positive number, the result of incrementing it is also positive. + +```kotlin +import arrow.analysis.pre +import arrow.analysis.post + +fun increment(x: Int): Int { + pre(x > 0) { "value must be positive" } + return (x + 1).post({ it > 0 }) { "result is positive" } +} +``` + +The post-condition is attached to the result value of the function. The first argument works in a special way: it should be a lambda whose argument represents the return value. You'll often see `{ it > 0 }` in this docs, but feel free to write it as `{ result -> result > 0 }` if that looks better for you. + +Most importantly, the error in the double call of `increment` is now gone! 😌 + +Λrrow Analysis does not blindly accept any post-condition you write, the tool ensure it's actually true. If you change it to `{ it < 0 }`, you get an error: + +``` +e: declaration `increment` fails to satisfy the post-condition: ($result < 0) +``` + +## Invariants in classes + +Imagine now that this notion of being positive occurs very often in your domain. It makes sense then to introduce a new _type_ for this concept, and to ensure that any usage obbeys the positiveness condition. In this case we talk about an _invariant_, something which is always true when using that particular type. + +Λrrow Analysis turns your `require`s in classes into checks at compile time. + +```kotlin +class Positive(val value: Int) { + init { require(value > 0) } +} +``` + +The following code is rejected with a very similar error to the ones above: + +```kotlin +val positiveExample = Positive(-1) +``` + +The tool is powerful enough to track the invariants of every value involved in a computation. For example, we can introduce an addition operation with two `Positive` numbers, and we can check statically that the result is again a positive number (otherwise we would not be allowed to construct an instance of `Positive`). + +```kotlin +fun Positive.add(other: Positive) = + Positive(this.value + other.value) +``` + +## Going further + +This Quick Start shows the basic features of Λrrow Analysis. The rest of the documentation describes all its features in depth, including thorough explanations about how to track information about properties of an object, and how to deal with mutability. \ No newline at end of file diff --git a/docs/docs/java/README.md b/docs/docs/java/README.md new file mode 100644 index 00000000..c89869ac --- /dev/null +++ b/docs/docs/java/README.md @@ -0,0 +1,105 @@ +--- +layout: docs-analysis +title: Analysis - Java support +permalink: /java/ +--- + +# Analysis over Java code + +⚠️ **This is still an alpha feature.** + +## Adding the plug-in + +Open your Gradle build file, and add the following lines: + +
+ +
+ + +
+ +
+ +```kotlin +buildscript { + dependencies { + classpath("io.arrow-kt.analysis.java:io.arrow-kt.analysis.java.gradle.plugin:2.0") + } +} + +apply(plugin = "io.arrow-kt.analysis.java") + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.0") +} +``` + +
+ +
+ +```groovy +buildscript { + dependencies { + classpath 'io.arrow-kt.analysis.java:io.arrow-kt.analysis.java.gradle.plugin:2.0' + } +} + +apply plugin: 'io.arrow-kt.analysis.java' + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.6.0' +} +``` + +
+
+ +Unfortunately, you have to depend on the Kotlin standard library explicitly; otherwise strange errors pop up during the compilation process. + +## Examples + +### Functions + +The usage of Λrrow Analysis in Java code is very similar to Kotlin. Note that you **must** import `pre` and `post` functions using `import static` for them to be considered by the plug-in. Following Kotlin's lead, the messages must be represented as lambdas, as we do below. + +```java +import static arrow.analysis.RefinementDSLKt.post; +import static arrow.analysis.RefinementDSLKt.pre; + +public class Example { + public int f(int x) { + pre(x > 0, () -> "x must be positive"); + return post(x + 1, r -> r > 0, () -> "result is positive"); + } +} +``` + +### Class invariants + +You can also define invariants for your class. In that case, the invariants go in so-called _instance initializers_, which are just blocks which appear within the class body. Note that mutable fields are **not** supported, so you need to mark those as `final`. + +```java +import static arrow.analysis.RefinementDSLKt.*; + +final class Positive { + private final int n; + + public Positive(int value) { + pre(value >= 0, () -> "value is positive"); + this.n = value; + } + + public int getValue() { + return n; + } + + { + // this is the class invariant + assert this.getValue() >= 0 : "value is positive"; + } +} +``` + +Note that Λrrow Analysis considers parameterless functions starting with `get` as fields. In the snippet above we use `getValue` in that fashion. \ No newline at end of file diff --git a/docs/docs/js/docsearch.min.js b/docs/docs/js/docsearch.min.js new file mode 100644 index 00000000..efb45b8f --- /dev/null +++ b/docs/docs/js/docsearch.min.js @@ -0,0 +1,2 @@ +/*! docsearch 2.6.3 | © Algolia | github.com/algolia/docsearch */ +(function webpackUniversalModuleDefinition(root,factory){if(typeof exports==="object"&&typeof module==="object")module.exports=factory();else if(typeof define==="function"&&define.amd)define([],factory);else if(typeof exports==="object")exports["docsearch"]=factory();else root["docsearch"]=factory()})(typeof self!=="undefined"?self:this,function(){return function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{configurable:false,enumerable:true,get:getter})}};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module["default"]}:function getModuleExports(){return module};__webpack_require__.d(getter,"a",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p="";return __webpack_require__(__webpack_require__.s=22)}([function(module,exports,__webpack_require__){"use strict";var DOM=__webpack_require__(1);function escapeRegExp(str){return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}module.exports={isArray:null,isFunction:null,isObject:null,bind:null,each:null,map:null,mixin:null,isMsie:function(agentString){if(agentString===undefined){agentString=navigator.userAgent}if(/(msie|trident)/i.test(agentString)){var match=agentString.match(/(msie |rv:)(\d+(.\d+)?)/i);if(match){return match[2]}}return false},escapeRegExChars:function(str){return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isNumber:function(obj){return typeof obj==="number"},toStr:function toStr(s){return s===undefined||s===null?"":s+""},cloneDeep:function cloneDeep(obj){var clone=this.mixin({},obj);var self=this;this.each(clone,function(value,key){if(value){if(self.isArray(value)){clone[key]=[].concat(value)}else if(self.isObject(value)){clone[key]=self.cloneDeep(value)}}});return clone},error:function(msg){throw new Error(msg)},every:function(obj,test){var result=true;if(!obj){return result}this.each(obj,function(val,key){if(result){result=test.call(null,val,key,obj)&&result}});return!!result},any:function(obj,test){var found=false;if(!obj){return found}this.each(obj,function(val,key){if(test.call(null,val,key,obj)){found=true;return false}});return found},getUniqueId:function(){var counter=0;return function(){return counter++}}(),templatify:function templatify(obj){if(this.isFunction(obj)){return obj}var $template=DOM.element(obj);if($template.prop("tagName")==="SCRIPT"){return function template(){return $template.text()}}return function template(){return String(obj)}},defer:function(fn){setTimeout(fn,0)},noop:function(){},formatPrefix:function(prefix,noPrefix){return noPrefix?"":prefix+"-"},className:function(prefix,clazz,skipDot){return(skipDot?"":".")+prefix+clazz},escapeHighlightedString:function(str,highlightPreTag,highlightPostTag){highlightPreTag=highlightPreTag||"";var pre=document.createElement("div");pre.appendChild(document.createTextNode(highlightPreTag));highlightPostTag=highlightPostTag||"";var post=document.createElement("div");post.appendChild(document.createTextNode(highlightPostTag));var div=document.createElement("div");div.appendChild(document.createTextNode(str));return div.innerHTML.replace(RegExp(escapeRegExp(pre.innerHTML),"g"),highlightPreTag).replace(RegExp(escapeRegExp(post.innerHTML),"g"),highlightPostTag)}}},function(module,exports,__webpack_require__){"use strict";module.exports={element:null}},function(module,exports){var hasOwn=Object.prototype.hasOwnProperty;var toString=Object.prototype.toString;module.exports=function forEach(obj,fn,ctx){if(toString.call(fn)!=="[object Function]"){throw new TypeError("iterator must be a function")}var l=obj.length;if(l===+l){for(var i=0;i was loaded but did not call our provided callback"),JSONPScriptError:createCustomError("JSONPScriptError","