Skip to content

Suggested changes.#1

Draft
bobg wants to merge 1 commit into
DanWlker:mainfrom
bobg:bobg/code-review
Draft

Suggested changes.#1
bobg wants to merge 1 commit into
DanWlker:mainfrom
bobg:bobg/code-review

Conversation

@bobg
Copy link
Copy Markdown

@bobg bobg commented Jul 8, 2024

Copy link
Copy Markdown
Author

@bobg bobg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this code looks very good - it's clear, well-laid-out, and looks correct.

Here are some suggestions about ways to make it more idiomatic, and in some places simpler and/or more efficient.

You should consider adding a bunch of unit tests.

Comment thread README.md
Installation (MacOs only currently):

brew install danwlker/remind/remind@0.0.2-alpha
```sh
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use this kind of Markdown quoting to show example shell commands.

Comment thread README.md
Alternatively, with Go installed:

```sh
go install github.com/DanWlker/remind@latest
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the standard way to install Go programs from source, if you have go installed yourself.

Comment thread go.mod
module github.com/DanWlker/remind

go 1.22.4
go 1.21
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Mingo, this code doesn't need a newer version of Go than 1.21 in order to compile.

Comment thread internal/app/add/add.go
errAddTodoAndAssociateTo := addTodoAndAssociateTo("", args)
if errAddTodoAndAssociateTo != nil {
return fmt.Errorf("addTodoAndAssociateTo: %w", errAddTodoAndAssociateTo)
if err := addTodoAndAssociateTo("", args); err != nil {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always use the name err for error variables, except in the rare case when you need to distinguish between multiple error values at the same time.

This name is so idiomatic that anything else is unnecessarily hard to read.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering about this, if there are multiple errors for a single function, whats the correct way of naming the different errors altogether? Is there a recommended format to stick to?

Copy link
Copy Markdown
Author

@bobg bobg Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just keep reusing err, as I've done here. This is safe as long as you're all finished with one value of err before setting it to a new value.

Of course if you do need to refer to an old error value, you'll need a different name. For example, you might want to accumulate errors with errors.Join, in which case your code will need to look something like this:

result1, err := firstOperation()
result2, err2 := secondOperation()
err = errors.Join(err, err2) // note, this deals with nils so you don't have to
...
return overallResult, err

Or if you prefer:

result1, err := firstOperation()
overallErr := err

result2, err := secondOperation()
overallErr = errors.Join(overallErr, err)
...
return overallResult, overallErr

Incidentally, the same thing goes for variables of type context.Context: they should almost always be named ctx, even when you're creating a new one from an old one with something like:

ctx, cancel := context.WithCancel(ctx)

Safe as long as you don't need the old value again, which in practice you seldom do.

Comment thread internal/app/add/add.go
homeRemCurrProExDir, errHomeRemCurrProExDir := shared.GetHomeRemovedWorkingDir()
if errHomeRemCurrProExDir != nil {
return fmt.Errorf("helper.GetHomeRemovedCurrentProgramExecutionDirectory: %w", errHomeRemCurrProExDir)
dir, err := shared.GetHomeRemovedWorkingDir()
Copy link
Copy Markdown
Author

@bobg bobg Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer short variable names, which paradoxically improve readability most of the time.

As a rule of thumb, the closer together the earliest and the latest uses of a variable name are, the shorter it can and should be. (Conversely, when uses of a variable are separated by a lot of space, you need to make the name more descriptive.)

func (e *FilePathNotStartsWithHome) Error() string {
return fmt.Sprintf("File path %v does not start with: %v", e.FileStr, e.HomeStr)
func (e NotUnderHomeError) Error() string {
return fmt.Sprintf("file path %v does not start with: %v", e.File, e.Home)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't capitalize error strings. See https://go.dev/wiki/CodeReviewComments#error-strings

Comment thread internal/record/record.go
func GetFileContents() ([]RecordEntity, error) {
recordFile, err := GetFile()
if err != nil {
return nil, fmt.Errorf("GetRecordFile: %w", err)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was previously returning []RecordEntity{}. But nil is the better way to express "an empty slice." It works the same way in all the contexts you care about - len, append, etc. - and unlike []RecordEntity{} does not allocate an empty backing array for the slice header to point to.

Comment thread internal/record/record.go
return RecordEntity{}, fmt.Errorf("GetDataFolder: %w", err)
}

newFile, err := os.CreateTemp(dataFolder, "*"+config.DefaultDataFileExtension)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's going to leak tempfiles. Who/what is responsible for cleaning these up?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was using this as a way to generate non conflicting file names directly inside the data folder, so in this case it counts as persistent data. Though I think it does not prevent this api from being used incorrectly. Perhaps i should split this into two different functions? Hmmm let me think about it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Clever. I totally overlooked that you were passing in dataFolder as the first argument. I'm so accustomed to that being "", and for this function being used exclusively for temp files (as the name implies).

In light of that misunderstanding, I suggest adding a comment here to prevent confusion in the next reader of this code - or in yourself when you return to this code years from now!

var editor = "vim"

func init() {
if s := os.Getenv("VISUAL"); s != "" {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use VISUAL in preference to EDITOR when available. See e.g. https://unix.stackexchange.com/questions/4859/visual-vs-editor-what-s-the-difference

}
defer os.Remove(tempFile.Name())

if _, err := tempFile.Write(data); err != nil {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was previously enclosed in an if len(data) != 0. But that test is unnecessary. If len(data) is 0, the call to tempFile.Write will just be a no-op.

@bobg bobg marked this pull request as draft July 8, 2024 05:36
@DanWlker
Copy link
Copy Markdown
Owner

DanWlker commented Jul 8, 2024

Hi wow first let me say thanks for taking the time to review it. I will go through it in the next few days. Thanks again

Comment thread internal/app/edit/edit.go
if item != "" {
var (
todoList []data.TodoEntity
sc = bufio.NewScanner(bytes.NewReader(result))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wow this is neat

Comment thread internal/config/config.go
const DEFAULT_CONFIG_FULL_FILE_NAME = string(os.PathSeparator) + DEFAULT_CONFIG_FILE_NAME + "." + DEFAULT_CONFIG_FILE_TYPE
DefaultConfigFileType = "yaml"
DefaultConfigFileName = ".remind"
DefaultConfigFullFileName = string(os.PathSeparator) + DefaultConfigFileName + "." + DefaultConfigFileType
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I actually started this thinking of using markdown as the data file type and yaml as the config file type, I think for the config case you're probably right its redundant

Comment thread internal/data/data.go
"strings"

"github.com/spf13/viper"
"gopkg.in/yaml.v3"
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have considered this library when searching for yaml library, but I saw its last commit was around 2 years ago so I'm not sure if its abandoned or the project is just complete as it is. So i went with the other one

Comment thread internal/cmd/add.go
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm this was intended to be a cli app only, I just kept the cmd folder there as that's where cobra added the files when running the cobra-cli add command

Comment thread internal/record/record.go
return RecordEntity{}, fmt.Errorf("GetDataFolder: %w", err)
}

newFile, err := os.CreateTemp(dataFolder, "*"+config.DefaultDataFileExtension)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was using this as a way to generate non conflicting file names directly inside the data folder, so in this case it counts as persistent data. Though I think it does not prevent this api from being used incorrectly. Perhaps i should split this into two different functions? Hmmm let me think about it

Comment thread internal/record/record.go
return fmt.Errorf("in yaml encoding: %w", err)
}

if err := f.Close(); err != nil {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why this extra call was here although there was a defer. Then i experimented with returning errors from defer, but I only managed to get it to work if the error is declared in the function signature

This works:

func oneFunc() (err error) {
	err = fmt.Errorf("error 1")
	defer func() {
		err = errors.Join(err, fmt.Errorf("error 2"))
	}()
	return err
}

This does not work:

func oneFunc() error {
	err := fmt.Errorf("error 1")
	defer func() {
		err = errors.Join(err, fmt.Errorf("error 2"))
	}()
	return err
}

Is this a good way to go about it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is. If you want a deferred function to affect the outer function's return value, then as you discovered, you need to use named result parameters. (See https://go.dev/wiki/CodeReviewComments#named-result-parameters.) And errors.Join is a good way to preserve both the error that made your function return (if any), and the error encountered while cleaning up.

Most of the time I prefer how simple defer f.Close() looks, and when the surrounding function returns due to an error, I'm OK losing any error from the Close call. But when the function is about to return normally, I prefer to capture any error from Close, which is why there's also an explicit (non-deferred) call near the end.

(In that case, the redundant deferred Close still runs, but it's a no-op since the file's already closed, and conveniently the "already-closed" error is disregarded.)

return "", fmt.Errorf("os.UserHomeDir: %w", err)
}

rel, err := filepath.Rel(home, filePathWithHome)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: This is neat

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants