Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs-linter): add lint for local links #2416

Merged
merged 34 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d578061
add checks for local files
leohhhn Jun 21, 2024
ded07f3
fix links
leohhhn Jun 21, 2024
7122f98
link
leohhhn Jun 21, 2024
937241a
links
leohhhn Jun 21, 2024
16d9d2e
fix comments
leohhhn Jun 21, 2024
d77c3b6
add comments
leohhhn Jun 21, 2024
ec370ac
fix comments
leohhhn Jun 21, 2024
9607c40
Apply suggestions from code review
leohhhn Jun 24, 2024
986d989
Merge branch 'master' into docs/linter-files
leohhhn Jun 24, 2024
25f9634
rename file
leohhhn Jun 24, 2024
3a44230
rename job
leohhhn Jun 24, 2024
66758ef
inline ifs
leohhhn Jun 24, 2024
2061e51
add better embedmd link check
leohhhn Jun 24, 2024
b1382e0
rename and remove redeclaration
leohhhn Jun 24, 2024
c89ef2d
rename maps
leohhhn Jun 24, 2024
b722ecf
org imports
leohhhn Jun 24, 2024
21c700d
redeclare
leohhhn Jun 24, 2024
767d375
lastindex
leohhhn Jun 24, 2024
1ae1b0b
add testcase
leohhhn Jun 24, 2024
8322650
Merge branch 'master' into docs/linter-files
leohhhn Jun 24, 2024
346667f
lastindex case
leohhhn Jun 24, 2024
de8cc5b
add case check for embedmd
leohhhn Jun 24, 2024
ec10dcc
add flow test
leohhhn Jun 25, 2024
d5190a0
rm typo
leohhhn Jun 25, 2024
0f84478
Merge branch 'master' into docs/linter-files
leohhhn Jun 26, 2024
d2ed755
update
leohhhn Jun 26, 2024
172bdba
update help
leohhhn Jun 26, 2024
4ddfd52
save
leohhhn Jun 26, 2024
99249d3
update tests
leohhhn Jun 26, 2024
a971146
add testcase
leohhhn Jun 26, 2024
64d6e5f
org imports
leohhhn Jun 26, 2024
91fb6ca
add lock for writing
leohhhn Jun 28, 2024
77b1e15
Merge branch 'refs/heads/master' into docs/linter-files
leohhhn Jun 28, 2024
04081ac
Merge branch 'refs/heads/master' into docs/linter-files
leohhhn Jun 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "docs / 404 checker"
name: "docs / lint"

on:
push:
Expand Down
9 changes: 5 additions & 4 deletions docs/how-to-guides/connecting-from-go.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The `gnoclient` package exposes a `Client` struct containing a `Signer` and
`RPCClient` connector. `Client` exposes all available functionality for talking
to a Gno.land chain.

```go
```go
type Client struct {
Signer Signer // Signer for transaction authentication
RPCClient rpcclient.Client // gnolang/gno/tm2/pkg/bft/rpc/client
Expand Down Expand Up @@ -101,8 +101,9 @@ func main() {
A few things to note:
- You can view keys in your local keybase by running `gnokey list`.
- You can get the password from a user input using the IO package.
- `Signer` can also be initialized in-memory from a BIP39 mnemonic, using the
[`SignerFromBip39`](../reference/gnoclient/signer.md#func-signerfrombip39) function.
- `Signer` can also be initialized in-memory from a BIP39 mnemonic, using the
[`SignerFromBip39`](https://gnolang.github.io/gno/github.com/gnolang/gno@v0.0.0/gno.land/pkg/gnoclient.html#SignerFromBip39)
function. function.

## Initialize the RPC connection & Client

Expand All @@ -116,7 +117,7 @@ if err != nil {
```

A list of Gno.land network endpoints & chain IDs can be found in the [Gno RPC
endpoints](../reference/rpc-endpoints.md#network-configurations) page.
endpoints](../reference/network-config.md) page.

With this, we can initialize the `gnoclient.Client` struct:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/gnoclient/gnoclient.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ To see the full reference documentation for the `gnoclient` package, we recommen
visiting the [`gnoclient godoc page`](https://gnolang.github.io/gno/github.com/gnolang/gno@v0.0.0/gno.land/pkg/gnoclient.html).

For a tutorial on how to use the `gnoclient` package, check out
["How to connect a Go app to Gno.land"](../../how-to-guides/connecting-from-go.md.)
["How to connect a Go app to Gno.land"](../../how-to-guides/connecting-from-go.md)

11 changes: 6 additions & 5 deletions misc/docs-linter/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package main
import "errors"

var (
errEmptyPath = errors.New("you need to pass in a path to scan")
err404Link = errors.New("link returned a 404")
errFound404Links = errors.New("found links resulting in a 404 response status")
errFoundUnescapedJSXTags = errors.New("found unescaped JSX tags")
errFoundLintItems = errors.New("found items that need linting")
errEmptyPath = errors.New("you need to pass in a path to scan")
err404Link = errors.New("link returned a 404")
errFound404Links = errors.New("found links resulting in a 404 response status")
errFoundUnescapedJSXTags = errors.New("found unescaped JSX tags")
errFoundUnreachableLocalLinks = errors.New("found local links that stat fails on")
errFoundLintItems = errors.New("found items that need linting")
)
7 changes: 3 additions & 4 deletions misc/docs-linter/jsx.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package main

import (
"context"
"fmt"
"regexp"
"strings"
)

// extractJSX extracts JSX tags from given file content
func extractJSX(fileContent []byte) []string {
text := string(fileContent)

Expand Down Expand Up @@ -34,10 +34,9 @@ func extractJSX(fileContent []byte) []string {
return filteredMatches
}

func lintJSX(fileUrlMap map[string][]string, ctx context.Context) error {
func lintJSX(filepathToJSX map[string][]string) error {
found := false
for filePath, tags := range fileUrlMap {
filePath := filePath
for filePath, tags := range filepathToJSX {
for _, tag := range tags {
if !found {
fmt.Println("Tags that need checking:")
Expand Down
119 changes: 46 additions & 73 deletions misc/docs-linter/links.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,105 +3,78 @@ package main
import (
"bufio"
"bytes"
"context"
"fmt"
"golang.org/x/sync/errgroup"
"io"
"mvdan.cc/xurls/v2"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
)

// extractUrls extracts URLs from a file and maps them to the file
func extractUrls(fileContent []byte) []string {
// extractLocalLinks extracts links to local files from the given file content
func extractLocalLinks(fileContent []byte) []string {
scanner := bufio.NewScanner(bytes.NewReader(fileContent))
urls := make([]string, 0)
links := make([]string, 0)
// Regular expression to match markdown links
re := regexp.MustCompile(`]\((\.\.?/.+?)\)`)

// Scan file line by line
for scanner.Scan() {
line := scanner.Text()

// Extract links
rxStrict := xurls.Strict()
url := rxStrict.FindString(line)
// Find embedmd links
if strings.Contains(line, "[embedmd]") {
openPar := strings.Index(line, "(")
deelawn marked this conversation as resolved.
Show resolved Hide resolved
closePar := strings.Index(line, ")")

// Check for empty links and skip them
if url == " " || len(url) == 0 {
link := line[openPar+1 : closePar]
if pos := strings.Index(link, " "); pos != -1 {
link = link[:pos]
}

links = append(links, link)
continue
}

// Look for http & https only
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
// Ignore localhost
if !strings.Contains(url, "localhost") && !strings.Contains(url, "127.0.0.1") {
urls = append(urls, url)
// Find all matches
matches := re.FindAllString(line, -1)

// Extract and print the local file links
for _, match := range matches {
// Remove ]( from the beginning and ) from end of link
match = match[2 : len(match)-1]

// Remove markdown headers in links
if pos := strings.Index(match, "#"); pos != -1 {
match = match[:pos]
}

links = append(links, match)
}
}

return urls
return links
}

func lintLinks(fileUrlMap map[string][]string, ctx context.Context) error {
// Filter links by prefix & ignore localhost
// Setup parallel checking for links
g, _ := errgroup.WithContext(ctx)

var (
lock sync.Mutex
notFoundUrls []string
)

for filePath, urls := range fileUrlMap {
filePath := filePath
for _, url := range urls {
url := url
g.Go(func() error {
if err := checkUrl(url); err != nil {
lock.Lock()
notFoundUrls = append(notFoundUrls, fmt.Sprintf(">>> %s (found in file: %s)", url, filePath))
lock.Unlock()
func lintLocalLinks(filepathToLinks map[string][]string, docsPath string) error {
var found bool
for filePath, links := range filepathToLinks {
for _, link := range links {
path := filepath.Join(docsPath, filepath.Dir(filePath), link)

if _, err := os.Stat(path); err != nil {
if !found {
fmt.Println("Could not find files with the following paths:")
found = true
}

return nil
})
fmt.Printf(">>> %s (found in file: %s)\n", link, filePath)
}
}
}

if err := g.Wait(); err != nil {
return err
}

// Print out the URLs that returned a 404 along with the file names
if len(notFoundUrls) > 0 {
fmt.Println("Links that need checking:")
for _, result := range notFoundUrls {
fmt.Println(result)
}

return errFound404Links
if found {
return errFoundUnreachableLocalLinks
}

return nil
}

// checkUrl checks if a URL is a 404
func checkUrl(url string) error {
// Attempt to retrieve the HTTP header
resp, err := http.Get(url)
if err != nil || resp.StatusCode == http.StatusNotFound {
return err404Link
}

// Ensure the response body is closed properly
cleanup := func(Body io.ReadCloser) error {
if err := Body.Close(); err != nil {
return fmt.Errorf("could not close response properly: %w", err)
}

return nil
}

return cleanup(resp.Body)
}
17 changes: 13 additions & 4 deletions misc/docs-linter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ func execLint(cfg *cfg, ctx context.Context) error {
}

// Make storage maps for tokens to analyze
fileUrlMap := make(map[string][]string) // file path > [urls]
fileJSXMap := make(map[string][]string) // file path > [JSX items]
fileUrlMap := make(map[string][]string) // file path > [urls]
fileJSXMap := make(map[string][]string) // file path > [JSX items]
fileLocalLinkMap := make(map[string][]string) // file path > [local links]

// Extract tokens from files
for _, filePath := range mdFiles {
Expand All @@ -72,21 +73,29 @@ func execLint(cfg *cfg, ctx context.Context) error {
return err
}

// Execute JSX extractor
fileJSXMap[filePath] = extractJSX(fileContents)

// Execute URL extractor
fileUrlMap[filePath] = extractUrls(fileContents)

// Execute local link extractor
fileLocalLinkMap[filePath] = extractLocalLinks(fileContents)
}

// Run linters in parallel
g, _ := errgroup.WithContext(ctx)

g.Go(func() error {
return lintJSX(fileJSXMap, ctx)
return lintJSX(fileJSXMap)
})

g.Go(func() error {
return lintURLs(fileUrlMap, ctx)
deelawn marked this conversation as resolved.
Show resolved Hide resolved
})

g.Go(func() error {
return lintLinks(fileUrlMap, ctx)
return lintLocalLinks(fileLocalLinkMap, cfg.docsPath)
})

if err := g.Wait(); err != nil {
Expand Down
36 changes: 36 additions & 0 deletions misc/docs-linter/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@ Returns **Promise<string>**
}
}

func TestExtractLocalLinks(t *testing.T) {
t.Parallel()

// Create mock file content with random local links
mockFileContent := `
Here is some text with a link to a local file: [text](../concepts/file1.md)
Here is another local link: [another](./path/to/file1.md)
Here is another local link: [another](./path/to/file2.md#header-1-2)
And a link to an external website: [example](https://example.com)
And a websocket link: [websocket](ws://example.com/socket)
Here's an embedmd link': [embedmd]:# (../assets/how-to-guides/simple-library/tapas.gno go)
`

// Expected JSX tags
expectedLinks := []string{
"../concepts/file1.md",
"./path/to/file1.md",
"./path/to/file2.md",
"../assets/how-to-guides/simple-library/tapas.gno",
}

// Extract local links tags from the mock file content
extractedLinks := extractLocalLinks([]byte(mockFileContent))

if len(expectedLinks) != len(extractedLinks) {
t.Fatal("did not extract the correct amount of local links")
}

sort.Strings(extractedLinks)
sort.Strings(expectedLinks)

for i, tag := range expectedLinks {
require.Equal(t, tag, extractedLinks[i])
}
}

func TestFindFilePaths(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading