Add labels to tasks (#45)
This commit is contained in:
parent
d39007baa0
commit
6b40df50d3
45 changed files with 9101 additions and 57 deletions
|
@ -100,7 +100,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||||
* [x] Tasks innerhalb eines definierbarem Bereich, sollte aber trotzdem der server machen, so à la "Gib mir alles für diesen Monat"
|
* [x] Tasks innerhalb eines definierbarem Bereich, sollte aber trotzdem der server machen, so à la "Gib mir alles für diesen Monat"
|
||||||
* [x] Bulk-edit -> Transactions
|
* [x] Bulk-edit -> Transactions
|
||||||
* [x] Assignees
|
* [x] Assignees
|
||||||
* [ ] Labels
|
* [x] Labels
|
||||||
* [ ] Attachments
|
* [ ] Attachments
|
||||||
* [ ] Task-Templates innerhalb namespaces und Listen (-> Mehrere, die auswählbar sind)
|
* [ ] Task-Templates innerhalb namespaces und Listen (-> Mehrere, die auswählbar sind)
|
||||||
* [ ] Ein Task muss von mehreren Assignees abgehakt werden bis er als done markiert wird
|
* [ ] Ein Task muss von mehreren Assignees abgehakt werden bis er als done markiert wird
|
||||||
|
@ -109,6 +109,8 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||||
### General features
|
### General features
|
||||||
|
|
||||||
* [x] Deps nach mod umziehen
|
* [x] Deps nach mod umziehen
|
||||||
|
* [ ] Performance bei rechtchecks verbessern
|
||||||
|
* User & Teamright sollte sich für n rechte in einer Funktion testen lassen
|
||||||
* [ ] Globale Limits für anlegbare Listen + Namespaces
|
* [ ] Globale Limits für anlegbare Listen + Namespaces
|
||||||
* [ ] "Smart Lists", Listen nach bestimmten Kriterien gefiltert -> nur UI?
|
* [ ] "Smart Lists", Listen nach bestimmten Kriterien gefiltert -> nur UI?
|
||||||
* [ ] "Performance-Statistik" -> Wie viele Tasks man in bestimmten Zeiträumen so geschafft hat etc
|
* [ ] "Performance-Statistik" -> Wie viele Tasks man in bestimmten Zeiträumen so geschafft hat etc
|
||||||
|
|
56
REST-Tests/labels.http
Normal file
56
REST-Tests/labels.http
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# Get all labels
|
||||||
|
GET http://localhost:8080/api/v1/labels
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
###
|
||||||
|
# Add a new label
|
||||||
|
PUT http://localhost:8080/api/v1/labels
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "test5"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
# Delete a label
|
||||||
|
DELETE http://localhost:8080/api/v1/labels/6
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
###
|
||||||
|
# Update a label
|
||||||
|
POST http://localhost:8080/api/v1/labels/1
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "testschinkenbrot",
|
||||||
|
"description": "käsebrot"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
# Get one label
|
||||||
|
GET http://localhost:8080/api/v1/labels/1
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
###
|
||||||
|
# Get all labels on a task
|
||||||
|
GET http://localhost:8080/api/v1/tasks/3565/labels
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
###
|
||||||
|
# Add a new label to a task
|
||||||
|
PUT http://localhost:8080/api/v1/tasks/3565/labels
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"label_id": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
# Delete a label from a task
|
||||||
|
DELETE http://localhost:8080/api/v1/tasks/3565/labels/1
|
||||||
|
Authorization: Bearer {{auth_token}}
|
||||||
|
|
||||||
|
###
|
535
docs/docs.go
535
docs/docs.go
|
@ -1,6 +1,6 @@
|
||||||
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||||
// This file was generated by swaggo/swag at
|
// This file was generated by swaggo/swag at
|
||||||
// 2018-12-29 15:14:06.225275112 +0100 CET m=+0.295589005
|
// 2018-12-30 21:42:08.56057367 +0100 CET m=+0.082542821
|
||||||
|
|
||||||
package docs
|
package docs
|
||||||
|
|
||||||
|
@ -25,6 +25,301 @@ var doc = `{
|
||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "/api/v1",
|
"basePath": "/api/v1",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/labels": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns all labels which are either created by the user or associated with a task the user has at least read-access to.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Get all labels a user has access to",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
|
||||||
|
"name": "p",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search labels by label text.",
|
||||||
|
"name": "s",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The labels",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Creates a new label.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Create a label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "The label object",
|
||||||
|
"name": "label",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The created label object.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid label object provided.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/labels/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns one label by its ID.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Gets one label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The label",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "The user does not have access to the label",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Update an existing label. The user needs to be the creator of the label to be able to do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Update a label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The label object",
|
||||||
|
"name": "label",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The created label object.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid label object provided.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to update the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Delete an existing label. The user needs to be the creator of the label to be able to do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Delete a label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The label was successfully deleted.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to delete the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/lists": {
|
"/lists": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -2349,6 +2644,205 @@ var doc = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/tasks/{task}/labels": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns all labels which are assicociated with a given task.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Get all labels on a task",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Task ID",
|
||||||
|
"name": "task",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
|
||||||
|
"name": "p",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search labels by label text.",
|
||||||
|
"name": "s",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The labels",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Add a label to a task. The user needs to have write-access to the list to be able do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Add a label to a task",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Task ID",
|
||||||
|
"name": "task",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The label object",
|
||||||
|
"name": "label",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The created label relation object.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid label object provided.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to add the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The label does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/tasks/{task}/labels/{label}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Remove a label from a task. The user needs to have write-access to the list to be able do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Remove a label from a task",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Task ID",
|
||||||
|
"name": "task",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "label",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The label was successfully removed.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to remove the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/teams": {
|
"/teams": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -3040,6 +3534,12 @@ var doc = `{
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
"listID": {
|
"listID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
@ -3089,6 +3589,33 @@ var doc = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"models.Label": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"created_by": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.User"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hex_color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"models.List": {
|
"models.List": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -3150,6 +3677,12 @@ var doc = `{
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
"listID": {
|
"listID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,6 +23,7 @@ This document describes the different errors Vikunja can return.
|
||||||
| 4002 | 404 | The list task does not exist. |
|
| 4002 | 404 | The list task does not exist. |
|
||||||
| 4003 | 403 | All bulk editing tasks must belong to the same list. |
|
| 4003 | 403 | All bulk editing tasks must belong to the same list. |
|
||||||
| 4004 | 403 | Need at least one task when bulk editing tasks. |
|
| 4004 | 403 | Need at least one task when bulk editing tasks. |
|
||||||
|
| 4005 | 403 | The user does not have the right to see the task. |
|
||||||
| 5001 | 404 | The namspace does not exist. |
|
| 5001 | 404 | The namspace does not exist. |
|
||||||
| 5003 | 403 | The user does not have access to the specified namespace. |
|
| 5003 | 403 | The user does not have access to the specified namespace. |
|
||||||
| 5006 | 400 | The namespace name cannot be empty. |
|
| 5006 | 400 | The namespace name cannot be empty. |
|
||||||
|
@ -39,3 +40,5 @@ This document describes the different errors Vikunja can return.
|
||||||
| 7001 | 400 | The user right is invalid. |
|
| 7001 | 400 | The user right is invalid. |
|
||||||
| 7002 | 409 | The user already has access to that list. |
|
| 7002 | 409 | The user already has access to that list. |
|
||||||
| 7003 | 403 | The user does not have access to that list. |
|
| 7003 | 403 | The user does not have access to that list. |
|
||||||
|
| 8001 | 403 | This label already exists on that task. |
|
||||||
|
| 8002 | 404 | The label does not exist. |
|
|
@ -12,6 +12,301 @@
|
||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "/api/v1",
|
"basePath": "/api/v1",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/labels": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns all labels which are either created by the user or associated with a task the user has at least read-access to.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Get all labels a user has access to",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
|
||||||
|
"name": "p",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search labels by label text.",
|
||||||
|
"name": "s",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The labels",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Creates a new label.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Create a label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "The label object",
|
||||||
|
"name": "label",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The created label object.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid label object provided.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/labels/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns one label by its ID.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Gets one label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The label",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "The user does not have access to the label",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Update an existing label. The user needs to be the creator of the label to be able to do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Update a label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The label object",
|
||||||
|
"name": "label",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The created label object.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid label object provided.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to update the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Delete an existing label. The user needs to be the creator of the label to be able to do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Delete a label",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The label was successfully deleted.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to delete the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/lists": {
|
"/lists": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -2336,6 +2631,205 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/tasks/{task}/labels": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Returns all labels which are assicociated with a given task.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Get all labels on a task",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Task ID",
|
||||||
|
"name": "task",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
|
||||||
|
"name": "p",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search labels by label text.",
|
||||||
|
"name": "s",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The labels",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Add a label to a task. The user needs to have write-access to the list to be able do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Add a label to a task",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Task ID",
|
||||||
|
"name": "task",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The label object",
|
||||||
|
"name": "label",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The created label relation object.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid label object provided.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to add the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "The label does not exist.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/tasks/{task}/labels/{label}": {
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Remove a label from a task. The user needs to have write-access to the list to be able do this.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"labels"
|
||||||
|
],
|
||||||
|
"summary": "Remove a label from a task",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Task ID",
|
||||||
|
"name": "task",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Label ID",
|
||||||
|
"name": "label",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The label was successfully removed.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not allowed to remove the label.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Label not found.",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal error",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/teams": {
|
"/teams": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -3026,6 +3520,12 @@
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
"listID": {
|
"listID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
@ -3075,6 +3575,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"models.Label": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"created_by": {
|
||||||
|
"type": "object",
|
||||||
|
"$ref": "#/definitions/models.User"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hex_color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"models.List": {
|
"models.List": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -3136,6 +3663,12 @@
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"labels": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/models.Label"
|
||||||
|
}
|
||||||
|
},
|
||||||
"listID": {
|
"listID": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,6 +32,10 @@ definitions:
|
||||||
type: integer
|
type: integer
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
|
labels:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: array
|
||||||
listID:
|
listID:
|
||||||
type: integer
|
type: integer
|
||||||
parentTaskID:
|
parentTaskID:
|
||||||
|
@ -64,6 +68,24 @@ definitions:
|
||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
models.Label:
|
||||||
|
properties:
|
||||||
|
created:
|
||||||
|
type: integer
|
||||||
|
created_by:
|
||||||
|
$ref: '#/definitions/models.User'
|
||||||
|
type: object
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
hex_color:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
updated:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
models.List:
|
models.List:
|
||||||
properties:
|
properties:
|
||||||
created:
|
created:
|
||||||
|
@ -105,6 +127,10 @@ definitions:
|
||||||
type: integer
|
type: integer
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
|
labels:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: array
|
||||||
listID:
|
listID:
|
||||||
type: integer
|
type: integer
|
||||||
parentTaskID:
|
parentTaskID:
|
||||||
|
@ -369,6 +395,205 @@ info:
|
||||||
title: Vikunja API
|
title: Vikunja API
|
||||||
version: '{{.Version}}'
|
version: '{{.Version}}'
|
||||||
paths:
|
paths:
|
||||||
|
/labels:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns all labels which are either created by the user or associated
|
||||||
|
with a task the user has at least read-access to.
|
||||||
|
parameters:
|
||||||
|
- description: The page number. Used for pagination. If not provided, the first
|
||||||
|
page of results is returned.
|
||||||
|
in: query
|
||||||
|
name: p
|
||||||
|
type: integer
|
||||||
|
- description: Search labels by label text.
|
||||||
|
in: query
|
||||||
|
name: s
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The labels
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Get all labels a user has access to
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Creates a new label.
|
||||||
|
parameters:
|
||||||
|
- description: The label object
|
||||||
|
in: body
|
||||||
|
name: label
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The created label object.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Invalid label object provided.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Create a label
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
/labels/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Delete an existing label. The user needs to be the creator of the
|
||||||
|
label to be able to do this.
|
||||||
|
parameters:
|
||||||
|
- description: Label ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The label was successfully deleted.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
"403":
|
||||||
|
description: Not allowed to delete the label.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Label not found.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Delete a label
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns one label by its ID.
|
||||||
|
parameters:
|
||||||
|
- description: Label ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The label
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
"403":
|
||||||
|
description: The user does not have access to the label
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Label not found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Gets one label
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Update an existing label. The user needs to be the creator of the
|
||||||
|
label to be able to do this.
|
||||||
|
parameters:
|
||||||
|
- description: Label ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The label object
|
||||||
|
in: body
|
||||||
|
name: label
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The created label object.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Invalid label object provided.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"403":
|
||||||
|
description: Not allowed to update the label.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Label not found.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Update a label
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
/lists:
|
/lists:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
@ -1750,6 +1975,141 @@ paths:
|
||||||
summary: Update a task
|
summary: Update a task
|
||||||
tags:
|
tags:
|
||||||
- task
|
- task
|
||||||
|
/tasks/{task}/labels:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Returns all labels which are assicociated with a given task.
|
||||||
|
parameters:
|
||||||
|
- description: Task ID
|
||||||
|
in: path
|
||||||
|
name: task
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The page number. Used for pagination. If not provided, the first
|
||||||
|
page of results is returned.
|
||||||
|
in: query
|
||||||
|
name: p
|
||||||
|
type: integer
|
||||||
|
- description: Search labels by label text.
|
||||||
|
in: query
|
||||||
|
name: s
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The labels
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Get all labels on a task
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Add a label to a task. The user needs to have write-access to the
|
||||||
|
list to be able do this.
|
||||||
|
parameters:
|
||||||
|
- description: Task ID
|
||||||
|
in: path
|
||||||
|
name: task
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: The label object
|
||||||
|
in: body
|
||||||
|
name: label
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The created label relation object.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Invalid label object provided.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"403":
|
||||||
|
description: Not allowed to add the label.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: The label does not exist.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Add a label to a task
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
|
/tasks/{task}/labels/{label}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Remove a label from a task. The user needs to have write-access
|
||||||
|
to the list to be able do this.
|
||||||
|
parameters:
|
||||||
|
- description: Task ID
|
||||||
|
in: path
|
||||||
|
name: task
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Label ID
|
||||||
|
in: path
|
||||||
|
name: label
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The label was successfully removed.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Label'
|
||||||
|
type: object
|
||||||
|
"403":
|
||||||
|
description: Not allowed to remove the label.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Label not found.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Message'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Remove a label from a task
|
||||||
|
tags:
|
||||||
|
- labels
|
||||||
/tasks/all:
|
/tasks/all:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -31,7 +31,7 @@ require (
|
||||||
github.com/go-openapi/swag v0.17.2 // indirect
|
github.com/go-openapi/swag v0.17.2 // indirect
|
||||||
github.com/go-redis/redis v6.14.2+incompatible
|
github.com/go-redis/redis v6.14.2+incompatible
|
||||||
github.com/go-sql-driver/mysql v1.4.1
|
github.com/go-sql-driver/mysql v1.4.1
|
||||||
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25 // indirect
|
github.com/go-xorm/builder v0.0.0-20170519032130-c8871c857d25
|
||||||
github.com/go-xorm/core v0.5.8
|
github.com/go-xorm/core v0.5.8
|
||||||
github.com/go-xorm/tests v0.5.6 // indirect
|
github.com/go-xorm/tests v0.5.6 // indirect
|
||||||
github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00
|
github.com/go-xorm/xorm v0.0.0-20170930012613-29d4a0330a00
|
||||||
|
|
|
@ -474,6 +474,34 @@ func (err ErrBulkTasksNeedAtLeastOne) HTTPError() web.HTTPError {
|
||||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksNeedAtLeastOne, Message: "Need at least one tasks to do bulk editing."}
|
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksNeedAtLeastOne, Message: "Need at least one tasks to do bulk editing."}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrNoRightToSeeTask represents an error where a user does not have the right to see a task
|
||||||
|
type ErrNoRightToSeeTask struct {
|
||||||
|
TaskID int64
|
||||||
|
UserID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrNoRightToSeeTask checks if an error is ErrNoRightToSeeTask.
|
||||||
|
func IsErrNoRightToSeeTask(err error) bool {
|
||||||
|
_, ok := err.(ErrNoRightToSeeTask)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrNoRightToSeeTask) Error() string {
|
||||||
|
return fmt.Sprintf("User does not have the right to see the task [TaskID: %v, UserID: %v]", err.TaskID, err.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCodeNoRightToSeeTask holds the unique world-error code of this error
|
||||||
|
const ErrCodeNoRightToSeeTask = 4005
|
||||||
|
|
||||||
|
// HTTPError holds the http error description
|
||||||
|
func (err ErrNoRightToSeeTask) HTTPError() web.HTTPError {
|
||||||
|
return web.HTTPError{
|
||||||
|
HTTPCode: http.StatusForbidden,
|
||||||
|
Code: ErrCodeNoRightToSeeTask,
|
||||||
|
Message: "You don't have the right to see this task.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =================
|
// =================
|
||||||
// Namespace errors
|
// Namespace errors
|
||||||
// =================
|
// =================
|
||||||
|
@ -864,3 +892,62 @@ const ErrCodeUserDoesNotHaveAccessToList = 7003
|
||||||
func (err ErrUserDoesNotHaveAccessToList) HTTPError() web.HTTPError {
|
func (err ErrUserDoesNotHaveAccessToList) HTTPError() web.HTTPError {
|
||||||
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToList, Message: "This user does not have access to the list."}
|
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToList, Message: "This user does not have access to the list."}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============
|
||||||
|
// Label errors
|
||||||
|
// =============
|
||||||
|
|
||||||
|
// ErrLabelIsAlreadyOnTask represents an error where a label is already bound to a task
|
||||||
|
type ErrLabelIsAlreadyOnTask struct {
|
||||||
|
LabelID int64
|
||||||
|
TaskID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrLabelIsAlreadyOnTask checks if an error is ErrLabelIsAlreadyOnTask.
|
||||||
|
func IsErrLabelIsAlreadyOnTask(err error) bool {
|
||||||
|
_, ok := err.(ErrLabelIsAlreadyOnTask)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrLabelIsAlreadyOnTask) Error() string {
|
||||||
|
return fmt.Sprintf("Label already exists on task [TaskID: %v, LabelID: %v]", err.TaskID, err.LabelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCodeLabelIsAlreadyOnTask holds the unique world-error code of this error
|
||||||
|
const ErrCodeLabelIsAlreadyOnTask = 8001
|
||||||
|
|
||||||
|
// HTTPError holds the http error description
|
||||||
|
func (err ErrLabelIsAlreadyOnTask) HTTPError() web.HTTPError {
|
||||||
|
return web.HTTPError{
|
||||||
|
HTTPCode: http.StatusBadRequest,
|
||||||
|
Code: ErrCodeLabelIsAlreadyOnTask,
|
||||||
|
Message: "This label already exists on the task.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrLabelDoesNotExist represents an error where a label does not exist
|
||||||
|
type ErrLabelDoesNotExist struct {
|
||||||
|
LabelID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrLabelDoesNotExist checks if an error is ErrLabelDoesNotExist.
|
||||||
|
func IsErrLabelDoesNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrLabelDoesNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrLabelDoesNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("Label does not exist [LabelID: %v]", err.LabelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCodeLabelDoesNotExist holds the unique world-error code of this error
|
||||||
|
const ErrCodeLabelDoesNotExist = 8002
|
||||||
|
|
||||||
|
// HTTPError holds the http error description
|
||||||
|
func (err ErrLabelDoesNotExist) HTTPError() web.HTTPError {
|
||||||
|
return web.HTTPError{
|
||||||
|
HTTPCode: http.StatusNotFound,
|
||||||
|
Code: ErrCodeLabelDoesNotExist,
|
||||||
|
Message: "This label does not exist.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
3
pkg/models/fixtures/label_task.yml
Normal file
3
pkg/models/fixtures/label_task.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
- id: 1
|
||||||
|
task_id: 1
|
||||||
|
label_id: 4
|
12
pkg/models/fixtures/labels.yml
Normal file
12
pkg/models/fixtures/labels.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
- id: 1
|
||||||
|
title: 'Label #1'
|
||||||
|
created_by_id: 1
|
||||||
|
- id: 2
|
||||||
|
title: 'Label #2'
|
||||||
|
created_by_id: 1
|
||||||
|
- id: 3
|
||||||
|
title: 'Label #3 - other user'
|
||||||
|
created_by_id: 2
|
||||||
|
- id: 4
|
||||||
|
title: 'Label #4 - visible via other task'
|
||||||
|
created_by_id: 2
|
|
@ -22,3 +22,9 @@
|
||||||
description: Lorem Ipsum
|
description: Lorem Ipsum
|
||||||
owner_id: 3
|
owner_id: 3
|
||||||
namespace_id: 3
|
namespace_id: 3
|
||||||
|
-
|
||||||
|
id: 5
|
||||||
|
title: Test5
|
||||||
|
description: Lorem Ipsum
|
||||||
|
owner_id: 5
|
||||||
|
namespace_id: 5
|
|
@ -85,3 +85,9 @@
|
||||||
list_id: 2
|
list_id: 2
|
||||||
created: 1543626724
|
created: 1543626724
|
||||||
updated: 1543626724
|
updated: 1543626724
|
||||||
|
- id: 14
|
||||||
|
text: 'task #14 basic other list'
|
||||||
|
created_by_id: 5
|
||||||
|
list_id: 5
|
||||||
|
created: 1543626724
|
||||||
|
updated: 1543626724
|
59
pkg/models/label.go
Normal file
59
pkg/models/label.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Vikunja is a todo-list application to facilitate your life.
|
||||||
|
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Label represents a label
|
||||||
|
type Label struct {
|
||||||
|
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"label"`
|
||||||
|
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(3|250)"`
|
||||||
|
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)"`
|
||||||
|
HexColor string `xorm:"varchar(6)" json:"hex_color" valid:"runelength(0|6)"`
|
||||||
|
|
||||||
|
CreatedByID int64 `xorm:"int(11) not null" json:"-"`
|
||||||
|
CreatedBy *User `xorm:"-" json:"created_by"`
|
||||||
|
|
||||||
|
Created int64 `xorm:"created" json:"created"`
|
||||||
|
Updated int64 `xorm:"updated" json:"updated"`
|
||||||
|
|
||||||
|
web.CRUDable `xorm:"-" json:"-"`
|
||||||
|
web.Rights `xorm:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName makes a pretty table name
|
||||||
|
func (Label) TableName() string {
|
||||||
|
return "labels"
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabelTask represents a relation between a label and a task
|
||||||
|
type LabelTask struct {
|
||||||
|
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
|
||||||
|
TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"`
|
||||||
|
LabelID int64 `xorm:"int(11) INDEX not null" json:"label_id" param:"label"`
|
||||||
|
Created int64 `xorm:"created" json:"created"`
|
||||||
|
|
||||||
|
web.CRUDable `xorm:"-" json:"-"`
|
||||||
|
web.Rights `xorm:"-" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName makes a pretty table name
|
||||||
|
func (LabelTask) TableName() string {
|
||||||
|
return "label_task"
|
||||||
|
}
|
87
pkg/models/label_create_update.go
Normal file
87
pkg/models/label_create_update.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
// Vikunja is a todo-list application to facilitate your life.
|
||||||
|
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "code.vikunja.io/web"
|
||||||
|
|
||||||
|
// Create creates a new label
|
||||||
|
// @Summary Create a label
|
||||||
|
// @Description Creates a new label.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Param label body models.Label true "The label object"
|
||||||
|
// @Success 200 {object} models.Label "The created label object."
|
||||||
|
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /labels [put]
|
||||||
|
func (l *Label) Create(a web.Auth) (err error) {
|
||||||
|
u, err := getUserWithError(a)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.CreatedBy = u
|
||||||
|
l.CreatedByID = u.ID
|
||||||
|
|
||||||
|
_, err = x.Insert(l)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates a label
|
||||||
|
// @Summary Update a label
|
||||||
|
// @Description Update an existing label. The user needs to be the creator of the label to be able to do this.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Param id path int true "Label ID"
|
||||||
|
// @Param label body models.Label true "The label object"
|
||||||
|
// @Success 200 {object} models.Label "The created label object."
|
||||||
|
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
|
||||||
|
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to update the label."
|
||||||
|
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /labels/{id} [put]
|
||||||
|
func (l *Label) Update() (err error) {
|
||||||
|
_, err = x.ID(l.ID).Update(l)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = l.ReadOne()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes a label
|
||||||
|
// @Summary Delete a label
|
||||||
|
// @Description Delete an existing label. The user needs to be the creator of the label to be able to do this.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Param id path int true "Label ID"
|
||||||
|
// @Success 200 {object} models.Label "The label was successfully deleted."
|
||||||
|
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to delete the label."
|
||||||
|
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /labels/{id} [delete]
|
||||||
|
func (l *Label) Delete() (err error) {
|
||||||
|
_, err = x.ID(l.ID).Delete(&Label{})
|
||||||
|
return err
|
||||||
|
}
|
106
pkg/models/label_read.go
Normal file
106
pkg/models/label_read.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// Vikunja is a todo-list application to facilitate your life.
|
||||||
|
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadAll gets all labels a user can use
|
||||||
|
// @Summary Get all labels a user has access to
|
||||||
|
// @Description Returns all labels which are either created by the user or associated with a task the user has at least read-access to.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||||
|
// @Param s query string false "Search labels by label text."
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 200 {array} models.Label "The labels"
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /labels [get]
|
||||||
|
func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, err error) {
|
||||||
|
u, err := getUserWithError(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all tasks
|
||||||
|
taskIDs, err := getUserTaskIDs(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLabelsByTaskIDs(search, u, page, taskIDs, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOne gets one label
|
||||||
|
// @Summary Gets one label
|
||||||
|
// @Description Returns one label by its ID.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Label ID"
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 200 {object} models.Label "The label"
|
||||||
|
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the label"
|
||||||
|
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found"
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /labels/{id} [get]
|
||||||
|
func (l *Label) ReadOne() (err error) {
|
||||||
|
label, err := getLabelByIDSimple(l.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*l = *label
|
||||||
|
|
||||||
|
user, err := GetUserByID(l.CreatedByID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.CreatedBy = &user
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLabelByIDSimple(labelID int64) (*Label, error) {
|
||||||
|
label := Label{}
|
||||||
|
exists, err := x.ID(labelID).Get(&label)
|
||||||
|
if err != nil {
|
||||||
|
return &label, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return &Label{}, ErrLabelDoesNotExist{labelID}
|
||||||
|
}
|
||||||
|
return &label, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get all task ids a user has
|
||||||
|
func getUserTaskIDs(u *User) (taskIDs []int64, err error) {
|
||||||
|
tasks, err := GetTasksByUser("", u, -1, SortTasksByUnsorted, time.Unix(0, 0), time.Unix(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a slice of task ids
|
||||||
|
for _, t := range tasks {
|
||||||
|
taskIDs = append(taskIDs, t.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
83
pkg/models/label_rights.go
Normal file
83
pkg/models/label_rights.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Vikunja is a todo-list application to facilitate your life.
|
||||||
|
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
"github.com/go-xorm/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanUpdate checks if a user can update a label
|
||||||
|
func (l *Label) CanUpdate(a web.Auth) bool {
|
||||||
|
return l.isLabelOwner(a) // Only owners should be allowed to update a label
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanDelete checks if a user can delete a label
|
||||||
|
func (l *Label) CanDelete(a web.Auth) bool {
|
||||||
|
return l.isLabelOwner(a) // Only owners should be allowed to delete a label
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanRead checks if a user can read a label
|
||||||
|
func (l *Label) CanRead(a web.Auth) bool {
|
||||||
|
return l.hasAccessToLabel(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanCreate checks if the user can create a label
|
||||||
|
// Currently a dummy.
|
||||||
|
func (l *Label) CanCreate(a web.Auth) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Label) isLabelOwner(a web.Auth) bool {
|
||||||
|
u := getUserForRights(a)
|
||||||
|
lorig, err := getLabelByIDSimple(l.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Log.Errorf("Error occurred during isLabelOwner for Label: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return lorig.CreatedByID == u.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to check if a user can see a specific label
|
||||||
|
func (l *Label) hasAccessToLabel(a web.Auth) bool {
|
||||||
|
u := getUserForRights(a)
|
||||||
|
|
||||||
|
// Get all tasks
|
||||||
|
taskIDs, err := getUserTaskIDs(u)
|
||||||
|
if err != nil {
|
||||||
|
log.Log.Errorf("Error occurred during hasAccessToLabel for Label: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all labels associated with these tasks
|
||||||
|
var labels []*Label
|
||||||
|
has, err := x.Table("labels").
|
||||||
|
Select("labels.*").
|
||||||
|
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
||||||
|
Where("label_task.label_id != null OR labels.created_by_id = ?", u.ID).
|
||||||
|
Or(builder.In("label_task.task_id", taskIDs)).
|
||||||
|
And("labels.id = ?", l.ID).
|
||||||
|
GroupBy("labels.id").
|
||||||
|
Exist(&labels)
|
||||||
|
if err != nil {
|
||||||
|
log.Log.Errorf("Error occurred during hasAccessToLabel for Label: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return has
|
||||||
|
}
|
153
pkg/models/label_task.go
Normal file
153
pkg/models/label_task.go
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
// Vikunja is a todo-list application to facilitate your life.
|
||||||
|
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
"github.com/go-xorm/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete deletes a label on a task
|
||||||
|
// @Summary Remove a label from a task
|
||||||
|
// @Description Remove a label from a task. The user needs to have write-access to the list to be able do this.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Param task path int true "Task ID"
|
||||||
|
// @Param label path int true "Label ID"
|
||||||
|
// @Success 200 {object} models.Label "The label was successfully removed."
|
||||||
|
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to remove the label."
|
||||||
|
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /tasks/{task}/labels/{label} [delete]
|
||||||
|
func (l *LabelTask) Delete() (err error) {
|
||||||
|
_, err = x.Delete(&LabelTask{LabelID: l.LabelID, TaskID: l.TaskID})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create adds a label to a task
|
||||||
|
// @Summary Add a label to a task
|
||||||
|
// @Description Add a label to a task. The user needs to have write-access to the list to be able do this.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Param task path int true "Task ID"
|
||||||
|
// @Param label body models.Label true "The label object"
|
||||||
|
// @Success 200 {object} models.Label "The created label relation object."
|
||||||
|
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
|
||||||
|
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to add the label."
|
||||||
|
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The label does not exist."
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /tasks/{task}/labels [put]
|
||||||
|
func (l *LabelTask) Create(a web.Auth) (err error) {
|
||||||
|
// Check if the label is already added
|
||||||
|
exists, err := x.Exist(&LabelTask{LabelID: l.LabelID, TaskID: l.TaskID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return ErrLabelIsAlreadyOnTask{l.LabelID, l.TaskID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert it
|
||||||
|
_, err = x.Insert(l)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAll gets all labels on a task
|
||||||
|
// @Summary Get all labels on a task
|
||||||
|
// @Description Returns all labels which are assicociated with a given task.
|
||||||
|
// @tags labels
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param task path int true "Task ID"
|
||||||
|
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||||
|
// @Param s query string false "Search labels by label text."
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Success 200 {array} models.Label "The labels"
|
||||||
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
// @Router /tasks/{task}/labels [get]
|
||||||
|
func (l *LabelTask) ReadAll(search string, a web.Auth, page int) (labels interface{}, err error) {
|
||||||
|
u, err := getUserWithError(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has the right to see the task
|
||||||
|
task, err := GetListTaskByID(l.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !task.CanRead(a) {
|
||||||
|
return nil, ErrNoRightToSeeTask{l.TaskID, u.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLabelsByTaskIDs(search, u, page, []int64{l.TaskID}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
type labelWithTaskID struct {
|
||||||
|
TaskID int64
|
||||||
|
Label `xorm:"extends"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get all labels for a set of tasks
|
||||||
|
// Used when getting all labels for one task as well when getting all lables
|
||||||
|
func getLabelsByTaskIDs(search string, u *User, page int, taskIDs []int64, getUnusedLabels bool) (ls []*labelWithTaskID, err error) {
|
||||||
|
// Incl unused labels
|
||||||
|
var uidOrNil interface{}
|
||||||
|
var requestOrNil interface{}
|
||||||
|
if getUnusedLabels {
|
||||||
|
uidOrNil = u.ID
|
||||||
|
requestOrNil = "label_task.label_id != null OR labels.created_by_id = ?"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all labels associated with these labels
|
||||||
|
var labels []*labelWithTaskID
|
||||||
|
err = x.Table("labels").
|
||||||
|
Select("labels.*, label_task.task_id").
|
||||||
|
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
||||||
|
Where(requestOrNil, uidOrNil).
|
||||||
|
Or(builder.In("label_task.task_id", taskIDs)).
|
||||||
|
And("labels.title LIKE ?", "%"+search+"%").
|
||||||
|
GroupBy("labels.id").
|
||||||
|
Limit(getLimitFromPageIndex(page)).
|
||||||
|
Find(&labels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all created by users
|
||||||
|
var userids []int64
|
||||||
|
for _, l := range labels {
|
||||||
|
userids = append(userids, l.CreatedByID)
|
||||||
|
}
|
||||||
|
users := make(map[int64]*User)
|
||||||
|
err = x.In("id", userids).Find(&users)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put it all together
|
||||||
|
for in, l := range labels {
|
||||||
|
labels[in].CreatedBy = users[l.CreatedByID]
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels, err
|
||||||
|
}
|
62
pkg/models/label_task_rights.go
Normal file
62
pkg/models/label_task_rights.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// Vikunja is a todo-list application to facilitate your life.
|
||||||
|
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CanCreate checks if a user can add a label to a task
|
||||||
|
func (lt *LabelTask) CanCreate(a web.Auth) bool {
|
||||||
|
label, err := getLabelByIDSimple(lt.LabelID)
|
||||||
|
if err != nil {
|
||||||
|
log.Log.Errorf("Error during CanCreate for LabelTask: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return label.hasAccessToLabel(a) && lt.canDoLabelTask(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanDelete checks if a user can delete a label from a task
|
||||||
|
func (lt *LabelTask) CanDelete(a web.Auth) bool {
|
||||||
|
if !lt.canDoLabelTask(a) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't care here if the label exists or not. The only relevant thing here is if the relation already exists,
|
||||||
|
// throw an error.
|
||||||
|
exists, err := x.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
|
||||||
|
if err != nil {
|
||||||
|
log.Log.Errorf("Error during CanDelete for LabelTask: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if a user can write to a task
|
||||||
|
// + is able to see the label
|
||||||
|
// always the same check for either deleting or adding a label to a task
|
||||||
|
func (lt *LabelTask) canDoLabelTask(a web.Auth) bool {
|
||||||
|
// A user can add a label to a task if he can write to the task
|
||||||
|
task, err := getTaskByIDSimple(lt.TaskID)
|
||||||
|
if err != nil {
|
||||||
|
log.Log.Error("Error occurred during canDoLabelTask for LabelTask: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return task.CanUpdate(a)
|
||||||
|
}
|
279
pkg/models/label_task_test.go
Normal file
279
pkg/models/label_task_test.go
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLabelTask_ReadAll(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
TaskID int64
|
||||||
|
LabelID int64
|
||||||
|
Created int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
search string
|
||||||
|
a web.Auth
|
||||||
|
page int
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantLabels interface{}
|
||||||
|
wantErr bool
|
||||||
|
errType func(error) bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 1,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
wantLabels: []*labelWithTaskID{
|
||||||
|
{
|
||||||
|
TaskID: 1,
|
||||||
|
Label: Label{
|
||||||
|
ID: 4,
|
||||||
|
Title: "Label #4 - visible via other task",
|
||||||
|
CreatedByID: 2,
|
||||||
|
CreatedBy: &User{
|
||||||
|
ID: 2,
|
||||||
|
Username: "user2",
|
||||||
|
Password: "1234",
|
||||||
|
Email: "user2@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no right to see the task",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 14,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errType: IsErrNoRightToSeeTask,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexistant task",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 9999,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errType: IsErrListTaskDoesNotExist,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &LabelTask{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
TaskID: tt.fields.TaskID,
|
||||||
|
LabelID: tt.fields.LabelID,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
gotLabels, err := l.ReadAll(tt.args.search, tt.args.a, tt.args.page)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("LabelTask.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||||
|
t.Errorf("LabelTask.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotLabels, tt.wantLabels) {
|
||||||
|
t.Errorf("LabelTask.ReadAll() = %v, want %v", gotLabels, tt.wantLabels)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabelTask_Create(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
TaskID int64
|
||||||
|
LabelID int64
|
||||||
|
Created int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
a web.Auth
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
errType func(error) bool
|
||||||
|
wantForbidden bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 1,
|
||||||
|
LabelID: 1,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already existing",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 1,
|
||||||
|
LabelID: 1,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errType: IsErrLabelIsAlreadyOnTask,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexisting label",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 1,
|
||||||
|
LabelID: 9999,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexisting task",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 9999,
|
||||||
|
LabelID: 1,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &LabelTask{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
TaskID: tt.fields.TaskID,
|
||||||
|
LabelID: tt.fields.LabelID,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
if !l.CanCreate(tt.args.a) && !tt.wantForbidden {
|
||||||
|
t.Errorf("LabelTask.CanCreate() forbidden, want %v", tt.wantForbidden)
|
||||||
|
}
|
||||||
|
err := l.Create(tt.args.a)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("LabelTask.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||||
|
t.Errorf("LabelTask.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabelTask_Delete(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
TaskID int64
|
||||||
|
LabelID int64
|
||||||
|
Created int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
errType func(error) bool
|
||||||
|
auth web.Auth
|
||||||
|
wantForbidden bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 1,
|
||||||
|
LabelID: 1,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete nonexistant",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 1,
|
||||||
|
LabelID: 1,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexisting label",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 1,
|
||||||
|
LabelID: 9999,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexisting task",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 9999,
|
||||||
|
LabelID: 1,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "existing, but forbidden task",
|
||||||
|
fields: fields{
|
||||||
|
TaskID: 14,
|
||||||
|
LabelID: 1,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &LabelTask{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
TaskID: tt.fields.TaskID,
|
||||||
|
LabelID: tt.fields.LabelID,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
if !l.CanDelete(tt.auth) && !tt.wantForbidden {
|
||||||
|
t.Errorf("LabelTask.CanDelete() forbidden, want %v", tt.wantForbidden)
|
||||||
|
}
|
||||||
|
err := l.Delete()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("LabelTask.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||||
|
t.Errorf("LabelTask.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
452
pkg/models/label_test.go
Normal file
452
pkg/models/label_test.go
Normal file
|
@ -0,0 +1,452 @@
|
||||||
|
// Vikunja is a todo-list application to facilitate your life.
|
||||||
|
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.vikunja.io/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLabel_ReadAll(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
HexColor string
|
||||||
|
CreatedByID int64
|
||||||
|
CreatedBy *User
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
search string
|
||||||
|
a web.Auth
|
||||||
|
page int
|
||||||
|
}
|
||||||
|
user1 := &User{
|
||||||
|
ID: 1,
|
||||||
|
Username: "user1",
|
||||||
|
Password: "1234",
|
||||||
|
Email: "user1@example.com",
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantLs interface{}
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
wantLs: []*labelWithTaskID{
|
||||||
|
{
|
||||||
|
Label: Label{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Label #1",
|
||||||
|
CreatedByID: 1,
|
||||||
|
CreatedBy: user1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: Label{
|
||||||
|
ID: 2,
|
||||||
|
Title: "Label #2",
|
||||||
|
CreatedByID: 1,
|
||||||
|
CreatedBy: user1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TaskID: 1,
|
||||||
|
Label: Label{
|
||||||
|
ID: 4,
|
||||||
|
Title: "Label #4 - visible via other task",
|
||||||
|
CreatedByID: 2,
|
||||||
|
CreatedBy: &User{
|
||||||
|
ID: 2,
|
||||||
|
Username: "user2",
|
||||||
|
Password: "1234",
|
||||||
|
Email: "user2@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid user",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &Label{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
Title: tt.fields.Title,
|
||||||
|
Description: tt.fields.Description,
|
||||||
|
HexColor: tt.fields.HexColor,
|
||||||
|
CreatedByID: tt.fields.CreatedByID,
|
||||||
|
CreatedBy: tt.fields.CreatedBy,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
Updated: tt.fields.Updated,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
gotLs, err := l.ReadAll(tt.args.search, tt.args.a, tt.args.page)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Label.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotLs, tt.wantLs) {
|
||||||
|
t.Errorf("Label.ReadAll() = %v, want %v", gotLs, tt.wantLs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabel_ReadOne(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
HexColor string
|
||||||
|
CreatedByID int64
|
||||||
|
CreatedBy *User
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
user1 := &User{
|
||||||
|
ID: 1,
|
||||||
|
Username: "user1",
|
||||||
|
Password: "1234",
|
||||||
|
Email: "user1@example.com",
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want *Label
|
||||||
|
wantErr bool
|
||||||
|
errType func(error) bool
|
||||||
|
auth web.Auth
|
||||||
|
wantForbidden bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Get label #1",
|
||||||
|
fields: fields{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
want: &Label{
|
||||||
|
ID: 1,
|
||||||
|
Title: "Label #1",
|
||||||
|
CreatedByID: 1,
|
||||||
|
CreatedBy: user1,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get nonexistant label",
|
||||||
|
fields: fields{
|
||||||
|
ID: 9999,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errType: IsErrLabelDoesNotExist,
|
||||||
|
wantForbidden: true,
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no rights",
|
||||||
|
fields: fields{
|
||||||
|
ID: 3,
|
||||||
|
},
|
||||||
|
wantForbidden: true,
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Get label #4 - other user",
|
||||||
|
fields: fields{
|
||||||
|
ID: 4,
|
||||||
|
},
|
||||||
|
want: &Label{
|
||||||
|
ID: 4,
|
||||||
|
Title: "Label #4 - visible via other task",
|
||||||
|
CreatedByID: 2,
|
||||||
|
CreatedBy: &User{
|
||||||
|
ID: 2,
|
||||||
|
Username: "user2",
|
||||||
|
Password: "1234",
|
||||||
|
Email: "user2@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &Label{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
Title: tt.fields.Title,
|
||||||
|
Description: tt.fields.Description,
|
||||||
|
HexColor: tt.fields.HexColor,
|
||||||
|
CreatedByID: tt.fields.CreatedByID,
|
||||||
|
CreatedBy: tt.fields.CreatedBy,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
Updated: tt.fields.Updated,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !l.CanRead(tt.auth) && !tt.wantForbidden {
|
||||||
|
t.Errorf("Label.CanRead() forbidden, want %v", tt.wantForbidden)
|
||||||
|
}
|
||||||
|
err := l.ReadOne()
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Label.ReadOne() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if (err != nil) && tt.wantErr && !tt.errType(err) {
|
||||||
|
t.Errorf("Label.ReadOne() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(l, tt.want) && !tt.wantErr && !tt.wantForbidden {
|
||||||
|
t.Errorf("Label.ReadOne() = %v, want %v", l, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabel_Create(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
HexColor string
|
||||||
|
CreatedByID int64
|
||||||
|
CreatedBy *User
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
a web.Auth
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
wantForbidden bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
fields: fields{
|
||||||
|
Title: "Test #1",
|
||||||
|
Description: "Lorem Ipsum",
|
||||||
|
HexColor: "ffccff",
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
a: &User{ID: 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &Label{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
Title: tt.fields.Title,
|
||||||
|
Description: tt.fields.Description,
|
||||||
|
HexColor: tt.fields.HexColor,
|
||||||
|
CreatedByID: tt.fields.CreatedByID,
|
||||||
|
CreatedBy: tt.fields.CreatedBy,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
Updated: tt.fields.Updated,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
if !l.CanCreate(tt.args.a) && !tt.wantForbidden {
|
||||||
|
t.Errorf("Label.CanCreate() forbidden, want %v", tt.wantForbidden)
|
||||||
|
}
|
||||||
|
if err := l.Create(tt.args.a); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Label.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabel_Update(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
HexColor string
|
||||||
|
CreatedByID int64
|
||||||
|
CreatedBy *User
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
auth web.Auth
|
||||||
|
wantForbidden bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
fields: fields{
|
||||||
|
ID: 1,
|
||||||
|
Title: "new and better",
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexisting",
|
||||||
|
fields: fields{
|
||||||
|
ID: 99999,
|
||||||
|
Title: "new and better",
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no rights",
|
||||||
|
fields: fields{
|
||||||
|
ID: 3,
|
||||||
|
Title: "new and better",
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no rights other creator but access",
|
||||||
|
fields: fields{
|
||||||
|
ID: 4,
|
||||||
|
Title: "new and better",
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &Label{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
Title: tt.fields.Title,
|
||||||
|
Description: tt.fields.Description,
|
||||||
|
HexColor: tt.fields.HexColor,
|
||||||
|
CreatedByID: tt.fields.CreatedByID,
|
||||||
|
CreatedBy: tt.fields.CreatedBy,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
Updated: tt.fields.Updated,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
if !l.CanUpdate(tt.auth) && !tt.wantForbidden {
|
||||||
|
t.Errorf("Label.CanUpdate() forbidden, want %v", tt.wantForbidden)
|
||||||
|
}
|
||||||
|
if err := l.Update(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Label.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabel_Delete(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID int64
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
HexColor string
|
||||||
|
CreatedByID int64
|
||||||
|
CreatedBy *User
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
CRUDable web.CRUDable
|
||||||
|
Rights web.Rights
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
wantErr bool
|
||||||
|
auth web.Auth
|
||||||
|
wantForbidden bool
|
||||||
|
}{
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
fields: fields{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nonexisting",
|
||||||
|
fields: fields{
|
||||||
|
ID: 99999,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true, // When the label does not exist, it is forbidden. We should fix this, but for everything.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no rights",
|
||||||
|
fields: fields{
|
||||||
|
ID: 3,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no rights but visible",
|
||||||
|
fields: fields{
|
||||||
|
ID: 4,
|
||||||
|
},
|
||||||
|
auth: &User{ID: 1},
|
||||||
|
wantForbidden: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
l := &Label{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
Title: tt.fields.Title,
|
||||||
|
Description: tt.fields.Description,
|
||||||
|
HexColor: tt.fields.HexColor,
|
||||||
|
CreatedByID: tt.fields.CreatedByID,
|
||||||
|
CreatedBy: tt.fields.CreatedBy,
|
||||||
|
Created: tt.fields.Created,
|
||||||
|
Updated: tt.fields.Updated,
|
||||||
|
CRUDable: tt.fields.CRUDable,
|
||||||
|
Rights: tt.fields.Rights,
|
||||||
|
}
|
||||||
|
if !l.CanDelete(tt.auth) && !tt.wantForbidden {
|
||||||
|
t.Errorf("Label.CanDelete() forbidden, want %v", tt.wantForbidden)
|
||||||
|
}
|
||||||
|
if err := l.Delete(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Label.Delete() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,20 +23,21 @@ import (
|
||||||
|
|
||||||
// ListTask represents an task in a todolist
|
// ListTask represents an task in a todolist
|
||||||
type ListTask struct {
|
type ListTask struct {
|
||||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
||||||
Text string `xorm:"varchar(250)" json:"text" valid:"runelength(3|250)"`
|
Text string `xorm:"varchar(250)" json:"text" valid:"runelength(3|250)"`
|
||||||
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)"`
|
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)"`
|
||||||
Done bool `xorm:"INDEX" json:"done"`
|
Done bool `xorm:"INDEX" json:"done"`
|
||||||
DueDateUnix int64 `xorm:"int(11) INDEX" json:"dueDate"`
|
DueDateUnix int64 `xorm:"int(11) INDEX" json:"dueDate"`
|
||||||
RemindersUnix []int64 `xorm:"JSON TEXT" json:"reminderDates"`
|
RemindersUnix []int64 `xorm:"JSON TEXT" json:"reminderDates"`
|
||||||
CreatedByID int64 `xorm:"int(11)" json:"-"` // ID of the user who put that task on the list
|
CreatedByID int64 `xorm:"int(11)" json:"-"` // ID of the user who put that task on the list
|
||||||
ListID int64 `xorm:"int(11) INDEX" json:"listID" param:"list"`
|
ListID int64 `xorm:"int(11) INDEX" json:"listID" param:"list"`
|
||||||
RepeatAfter int64 `xorm:"int(11) INDEX" json:"repeatAfter"`
|
RepeatAfter int64 `xorm:"int(11) INDEX" json:"repeatAfter"`
|
||||||
ParentTaskID int64 `xorm:"int(11) INDEX" json:"parentTaskID"`
|
ParentTaskID int64 `xorm:"int(11) INDEX" json:"parentTaskID"`
|
||||||
Priority int64 `xorm:"int(11)" json:"priority"`
|
Priority int64 `xorm:"int(11)" json:"priority"`
|
||||||
StartDateUnix int64 `xorm:"int(11) INDEX" json:"startDate"`
|
StartDateUnix int64 `xorm:"int(11) INDEX" json:"startDate"`
|
||||||
EndDateUnix int64 `xorm:"int(11) INDEX" json:"endDate"`
|
EndDateUnix int64 `xorm:"int(11) INDEX" json:"endDate"`
|
||||||
Assignees []*User `xorm:"-" json:"assignees"`
|
Assignees []*User `xorm:"-" json:"assignees"`
|
||||||
|
Labels []*Label `xorm:"-" json:"labels"`
|
||||||
|
|
||||||
Sorting string `xorm:"-" json:"-" param:"sort"` // Parameter to sort by
|
Sorting string `xorm:"-" json:"-" param:"sort"` // Parameter to sort by
|
||||||
StartDateSortUnix int64 `xorm:"-" json:"-" param:"startdatefilter"`
|
StartDateSortUnix int64 `xorm:"-" json:"-" param:"startdatefilter"`
|
||||||
|
@ -61,8 +62,8 @@ func (ListTask) TableName() string {
|
||||||
// ListTaskAssginee represents an assignment of a user to a task
|
// ListTaskAssginee represents an assignment of a user to a task
|
||||||
type ListTaskAssginee struct {
|
type ListTaskAssginee struct {
|
||||||
ID int64 `xorm:"int(11) autoincr not null unique pk"`
|
ID int64 `xorm:"int(11) autoincr not null unique pk"`
|
||||||
TaskID int64 `xorm:"int(11) not null"`
|
TaskID int64 `xorm:"int(11) INDEX not null"`
|
||||||
UserID int64 `xorm:"int(11) not null"`
|
UserID int64 `xorm:"int(11) INDEX not null"`
|
||||||
Created int64 `xorm:"created"`
|
Created int64 `xorm:"created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,37 +80,24 @@ type ListTaskAssigneeWithUser struct {
|
||||||
|
|
||||||
// GetTasksByListID gets all todotasks for a list
|
// GetTasksByListID gets all todotasks for a list
|
||||||
func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||||
err = x.Where("list_id = ?", listID).Find(&tasks)
|
// make a map so we can put in a lot of other stuff more easily
|
||||||
|
taskMap := make(map[int64]*ListTask, len(tasks))
|
||||||
|
err = x.Where("list_id = ?", listID).Find(&taskMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to iterate over users if the list doesn't has tasks
|
// No need to iterate over users and stuff if the list doesn't has tasks
|
||||||
if len(tasks) == 0 {
|
if len(taskMap) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a map so we can put in subtasks more easily
|
|
||||||
taskMap := make(map[int64]*ListTask, len(tasks))
|
|
||||||
|
|
||||||
// Get all users & task ids and put them into the array
|
// Get all users & task ids and put them into the array
|
||||||
var userIDs []int64
|
var userIDs []int64
|
||||||
var taskIDs []int64
|
var taskIDs []int64
|
||||||
for _, i := range tasks {
|
for _, i := range taskMap {
|
||||||
taskIDs = append(taskIDs, i.ID)
|
taskIDs = append(taskIDs, i.ID)
|
||||||
found := false
|
userIDs = append(userIDs, i.CreatedByID)
|
||||||
for _, u := range userIDs {
|
|
||||||
if i.CreatedByID == u {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
userIDs = append(userIDs, i.CreatedByID)
|
|
||||||
}
|
|
||||||
|
|
||||||
taskMap[i.ID] = i
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all assignees
|
// Get all assignees
|
||||||
|
@ -124,7 +112,18 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var users []User
|
// Get all labels for the tasks
|
||||||
|
labels, err := getLabelsByTaskIDs("", &User{}, -1, taskIDs, false)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, l := range labels {
|
||||||
|
if l != nil {
|
||||||
|
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &l.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users := make(map[int64]*User)
|
||||||
err = x.In("id", userIDs).Find(&users)
|
err = x.In("id", userIDs).Find(&users)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -134,12 +133,7 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||||
for _, task := range taskMap {
|
for _, task := range taskMap {
|
||||||
|
|
||||||
// Make created by user objects
|
// Make created by user objects
|
||||||
for _, u := range users {
|
taskMap[task.ID].CreatedBy = *users[task.CreatedByID]
|
||||||
if task.CreatedByID == u.ID {
|
|
||||||
taskMap[task.ID].CreatedBy = u
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reorder all subtasks
|
// Reorder all subtasks
|
||||||
if task.ParentTaskID != 0 {
|
if task.ParentTaskID != 0 {
|
||||||
|
@ -154,7 +148,7 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||||
tasks = append(tasks, t)
|
tasks = append(tasks, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the output. In Go, contents on a map are put on that map in no particular order.
|
// Sort the output. In Go, contents on a map are put on that map in no particular order (saved on heap).
|
||||||
// Because of this, tasks are not sorted anymore in the output, this leads to confiusion.
|
// Because of this, tasks are not sorted anymore in the output, this leads to confiusion.
|
||||||
// To avoid all this, we need to sort the slice afterwards
|
// To avoid all this, we need to sort the slice afterwards
|
||||||
sort.Slice(tasks, func(i, j int) bool {
|
sort.Slice(tasks, func(i, j int) bool {
|
||||||
|
@ -174,19 +168,27 @@ func getRawTaskAssigneesForTasks(taskIDs []int64) (taskAssignees []*ListTaskAssi
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetListTaskByID returns all tasks a list has
|
func getTaskByIDSimple(taskID int64) (task ListTask, err error) {
|
||||||
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
if taskID < 1 {
|
||||||
if listTaskID < 1 {
|
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
||||||
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exists, err := x.ID(listTaskID).Get(&listTask)
|
exists, err := x.ID(taskID).Get(&task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListTask{}, err
|
return ListTask{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
|
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListTaskByID returns all tasks a list has
|
||||||
|
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||||
|
listTask, err = getTaskByIDSimple(listTaskID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := GetUserByID(listTask.CreatedByID)
|
u, err := GetUserByID(listTask.CreatedByID)
|
||||||
|
|
|
@ -43,15 +43,19 @@ func (t *ListTask) CanUpdate(a web.Auth) bool {
|
||||||
doer := getUserForRights(a)
|
doer := getUserForRights(a)
|
||||||
|
|
||||||
// Get the task
|
// Get the task
|
||||||
lI, err := GetListTaskByID(t.ID)
|
lI, err := getTaskByIDSimple(t.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
|
log.Log.Error("Error occurred during CanUpdate (getTaskByIDSimple) for ListTask: %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// A user can update an task if he has write acces to its list
|
// A user can update an task if he has write acces to its list
|
||||||
l := &List{ID: lI.ListID}
|
l := &List{ID: lI.ListID}
|
||||||
l.ReadOne()
|
err = l.GetSimpleByID()
|
||||||
|
if err != nil {
|
||||||
|
log.Log.Error("Error occurred during CanUpdate (ReadOne) for ListTask: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
return l.CanWrite(doer)
|
return l.CanWrite(doer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,3 +68,10 @@ func (t *ListTask) CanCreate(a web.Auth) bool {
|
||||||
l.ReadOne()
|
l.ReadOne()
|
||||||
return l.CanWrite(doer)
|
return l.CanWrite(doer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanRead determines if a user can read a task
|
||||||
|
func (t *ListTask) CanRead(a web.Auth) bool {
|
||||||
|
// A user can read a task if it has access to the list
|
||||||
|
list := &List{ID: t.ListID}
|
||||||
|
return list.CanRead(a)
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,8 @@ func init() {
|
||||||
new(ListUser),
|
new(ListUser),
|
||||||
new(NamespaceUser),
|
new(NamespaceUser),
|
||||||
new(ListTaskAssginee),
|
new(ListTaskAssginee),
|
||||||
|
new(Label),
|
||||||
|
new(LabelTask),
|
||||||
)
|
)
|
||||||
|
|
||||||
tablesWithPointer = append(tables,
|
tablesWithPointer = append(tables,
|
||||||
|
@ -83,6 +85,8 @@ func init() {
|
||||||
&ListUser{},
|
&ListUser{},
|
||||||
&NamespaceUser{},
|
&NamespaceUser{},
|
||||||
&ListTaskAssginee{},
|
&ListTaskAssginee{},
|
||||||
|
&Label{},
|
||||||
|
&LabelTask{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -229,6 +229,26 @@ func RegisterRoutes(e *echo.Echo) {
|
||||||
}
|
}
|
||||||
a.POST("/tasks/bulk", bulkTaskHandler.UpdateWeb)
|
a.POST("/tasks/bulk", bulkTaskHandler.UpdateWeb)
|
||||||
|
|
||||||
|
labelTaskHandler := &handler.WebHandler{
|
||||||
|
EmptyStruct: func() handler.CObject {
|
||||||
|
return &models.LabelTask{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.PUT("/tasks/:listtask/labels", labelTaskHandler.CreateWeb)
|
||||||
|
a.DELETE("/tasks/:listtask/labels/:label", labelTaskHandler.DeleteWeb)
|
||||||
|
a.GET("/tasks/:listtask/labels", labelTaskHandler.ReadAllWeb)
|
||||||
|
|
||||||
|
labelHandler := &handler.WebHandler{
|
||||||
|
EmptyStruct: func() handler.CObject {
|
||||||
|
return &models.Label{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.GET("/labels", labelHandler.ReadAllWeb)
|
||||||
|
a.GET("/labels/:label", labelHandler.ReadOneWeb)
|
||||||
|
a.PUT("/labels", labelHandler.CreateWeb)
|
||||||
|
a.DELETE("/labels/:label", labelHandler.DeleteWeb)
|
||||||
|
a.POST("/labels/:label", labelHandler.UpdateWeb)
|
||||||
|
|
||||||
listTeamHandler := &handler.WebHandler{
|
listTeamHandler := &handler.WebHandler{
|
||||||
EmptyStruct: func() handler.CObject {
|
EmptyStruct: func() handler.CObject {
|
||||||
return &models.TeamList{}
|
return &models.TeamList{}
|
||||||
|
|
129
vendor/honnef.co/go/tools/callgraph/callgraph.go
vendored
Normal file
129
vendor/honnef.co/go/tools/callgraph/callgraph.go
vendored
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Package callgraph defines the call graph and various algorithms
|
||||||
|
and utilities to operate on it.
|
||||||
|
|
||||||
|
A call graph is a labelled directed graph whose nodes represent
|
||||||
|
functions and whose edge labels represent syntactic function call
|
||||||
|
sites. The presence of a labelled edge (caller, site, callee)
|
||||||
|
indicates that caller may call callee at the specified call site.
|
||||||
|
|
||||||
|
A call graph is a multigraph: it may contain multiple edges (caller,
|
||||||
|
*, callee) connecting the same pair of nodes, so long as the edges
|
||||||
|
differ by label; this occurs when one function calls another function
|
||||||
|
from multiple call sites. Also, it may contain multiple edges
|
||||||
|
(caller, site, *) that differ only by callee; this indicates a
|
||||||
|
polymorphic call.
|
||||||
|
|
||||||
|
A SOUND call graph is one that overapproximates the dynamic calling
|
||||||
|
behaviors of the program in all possible executions. One call graph
|
||||||
|
is more PRECISE than another if it is a smaller overapproximation of
|
||||||
|
the dynamic behavior.
|
||||||
|
|
||||||
|
All call graphs have a synthetic root node which is responsible for
|
||||||
|
calling main() and init().
|
||||||
|
|
||||||
|
Calls to built-in functions (e.g. panic, println) are not represented
|
||||||
|
in the call graph; they are treated like built-in operators of the
|
||||||
|
language.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package callgraph // import "honnef.co/go/tools/callgraph"
|
||||||
|
|
||||||
|
// TODO(adonovan): add a function to eliminate wrappers from the
|
||||||
|
// callgraph, preserving topology.
|
||||||
|
// More generally, we could eliminate "uninteresting" nodes such as
|
||||||
|
// nodes from packages we don't care about.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/token"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Graph represents a call graph.
|
||||||
|
//
|
||||||
|
// A graph may contain nodes that are not reachable from the root.
|
||||||
|
// If the call graph is sound, such nodes indicate unreachable
|
||||||
|
// functions.
|
||||||
|
//
|
||||||
|
type Graph struct {
|
||||||
|
Root *Node // the distinguished root node
|
||||||
|
Nodes map[*ssa.Function]*Node // all nodes by function
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Graph with the specified root node.
|
||||||
|
func New(root *ssa.Function) *Graph {
|
||||||
|
g := &Graph{Nodes: make(map[*ssa.Function]*Node)}
|
||||||
|
g.Root = g.CreateNode(root)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNode returns the Node for fn, creating it if not present.
|
||||||
|
func (g *Graph) CreateNode(fn *ssa.Function) *Node {
|
||||||
|
n, ok := g.Nodes[fn]
|
||||||
|
if !ok {
|
||||||
|
n = &Node{Func: fn, ID: len(g.Nodes)}
|
||||||
|
g.Nodes[fn] = n
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Node represents a node in a call graph.
|
||||||
|
type Node struct {
|
||||||
|
Func *ssa.Function // the function this node represents
|
||||||
|
ID int // 0-based sequence number
|
||||||
|
In []*Edge // unordered set of incoming call edges (n.In[*].Callee == n)
|
||||||
|
Out []*Edge // unordered set of outgoing call edges (n.Out[*].Caller == n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) String() string {
|
||||||
|
return fmt.Sprintf("n%d:%s", n.ID, n.Func)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Edge represents an edge in the call graph.
|
||||||
|
//
|
||||||
|
// Site is nil for edges originating in synthetic or intrinsic
|
||||||
|
// functions, e.g. reflect.Call or the root of the call graph.
|
||||||
|
type Edge struct {
|
||||||
|
Caller *Node
|
||||||
|
Site ssa.CallInstruction
|
||||||
|
Callee *Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) String() string {
|
||||||
|
return fmt.Sprintf("%s --> %s", e.Caller, e.Callee)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) Description() string {
|
||||||
|
var prefix string
|
||||||
|
switch e.Site.(type) {
|
||||||
|
case nil:
|
||||||
|
return "synthetic call"
|
||||||
|
case *ssa.Go:
|
||||||
|
prefix = "concurrent "
|
||||||
|
case *ssa.Defer:
|
||||||
|
prefix = "deferred "
|
||||||
|
}
|
||||||
|
return prefix + e.Site.Common().Description()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Edge) Pos() token.Pos {
|
||||||
|
if e.Site == nil {
|
||||||
|
return token.NoPos
|
||||||
|
}
|
||||||
|
return e.Site.Pos()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEdge adds the edge (caller, site, callee) to the call graph.
|
||||||
|
// Elimination of duplicate edges is the caller's responsibility.
|
||||||
|
func AddEdge(caller *Node, site ssa.CallInstruction, callee *Node) {
|
||||||
|
e := &Edge{caller, site, callee}
|
||||||
|
callee.In = append(callee.In, e)
|
||||||
|
caller.Out = append(caller.Out, e)
|
||||||
|
}
|
35
vendor/honnef.co/go/tools/callgraph/static/static.go
vendored
Normal file
35
vendor/honnef.co/go/tools/callgraph/static/static.go
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Package static computes the call graph of a Go program containing
|
||||||
|
// only static call edges.
|
||||||
|
package static // import "honnef.co/go/tools/callgraph/static"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"honnef.co/go/tools/callgraph"
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
"honnef.co/go/tools/ssa/ssautil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CallGraph computes the call graph of the specified program
|
||||||
|
// considering only static calls.
|
||||||
|
//
|
||||||
|
func CallGraph(prog *ssa.Program) *callgraph.Graph {
|
||||||
|
cg := callgraph.New(nil) // TODO(adonovan) eliminate concept of rooted callgraph
|
||||||
|
|
||||||
|
// TODO(adonovan): opt: use only a single pass over the ssa.Program.
|
||||||
|
// TODO(adonovan): opt: this is slower than RTA (perhaps because
|
||||||
|
// the lower precision means so many edges are allocated)!
|
||||||
|
for f := range ssautil.AllFunctions(prog) {
|
||||||
|
fnode := cg.CreateNode(f)
|
||||||
|
for _, b := range f.Blocks {
|
||||||
|
for _, instr := range b.Instrs {
|
||||||
|
if site, ok := instr.(ssa.CallInstruction); ok {
|
||||||
|
if g := site.Common().StaticCallee(); g != nil {
|
||||||
|
gnode := cg.CreateNode(g)
|
||||||
|
callgraph.AddEdge(fnode, site, gnode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cg
|
||||||
|
}
|
181
vendor/honnef.co/go/tools/callgraph/util.go
vendored
Normal file
181
vendor/honnef.co/go/tools/callgraph/util.go
vendored
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
// Copyright 2013 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package callgraph
|
||||||
|
|
||||||
|
import "honnef.co/go/tools/ssa"
|
||||||
|
|
||||||
|
// This file provides various utilities over call graphs, such as
|
||||||
|
// visitation and path search.
|
||||||
|
|
||||||
|
// CalleesOf returns a new set containing all direct callees of the
|
||||||
|
// caller node.
|
||||||
|
//
|
||||||
|
func CalleesOf(caller *Node) map[*Node]bool {
|
||||||
|
callees := make(map[*Node]bool)
|
||||||
|
for _, e := range caller.Out {
|
||||||
|
callees[e.Callee] = true
|
||||||
|
}
|
||||||
|
return callees
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphVisitEdges visits all the edges in graph g in depth-first order.
|
||||||
|
// The edge function is called for each edge in postorder. If it
|
||||||
|
// returns non-nil, visitation stops and GraphVisitEdges returns that
|
||||||
|
// value.
|
||||||
|
//
|
||||||
|
func GraphVisitEdges(g *Graph, edge func(*Edge) error) error {
|
||||||
|
seen := make(map[*Node]bool)
|
||||||
|
var visit func(n *Node) error
|
||||||
|
visit = func(n *Node) error {
|
||||||
|
if !seen[n] {
|
||||||
|
seen[n] = true
|
||||||
|
for _, e := range n.Out {
|
||||||
|
if err := visit(e.Callee); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := edge(e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, n := range g.Nodes {
|
||||||
|
if err := visit(n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathSearch finds an arbitrary path starting at node start and
|
||||||
|
// ending at some node for which isEnd() returns true. On success,
|
||||||
|
// PathSearch returns the path as an ordered list of edges; on
|
||||||
|
// failure, it returns nil.
|
||||||
|
//
|
||||||
|
func PathSearch(start *Node, isEnd func(*Node) bool) []*Edge {
|
||||||
|
stack := make([]*Edge, 0, 32)
|
||||||
|
seen := make(map[*Node]bool)
|
||||||
|
var search func(n *Node) []*Edge
|
||||||
|
search = func(n *Node) []*Edge {
|
||||||
|
if !seen[n] {
|
||||||
|
seen[n] = true
|
||||||
|
if isEnd(n) {
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
for _, e := range n.Out {
|
||||||
|
stack = append(stack, e) // push
|
||||||
|
if found := search(e.Callee); found != nil {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
stack = stack[:len(stack)-1] // pop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return search(start)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSyntheticNodes removes from call graph g all nodes for
|
||||||
|
// synthetic functions (except g.Root and package initializers),
|
||||||
|
// preserving the topology. In effect, calls to synthetic wrappers
|
||||||
|
// are "inlined".
|
||||||
|
//
|
||||||
|
func (g *Graph) DeleteSyntheticNodes() {
|
||||||
|
// Measurements on the standard library and go.tools show that
|
||||||
|
// resulting graph has ~15% fewer nodes and 4-8% fewer edges
|
||||||
|
// than the input.
|
||||||
|
//
|
||||||
|
// Inlining a wrapper of in-degree m, out-degree n adds m*n
|
||||||
|
// and removes m+n edges. Since most wrappers are monomorphic
|
||||||
|
// (n=1) this results in a slight reduction. Polymorphic
|
||||||
|
// wrappers (n>1), e.g. from embedding an interface value
|
||||||
|
// inside a struct to satisfy some interface, cause an
|
||||||
|
// increase in the graph, but they seem to be uncommon.
|
||||||
|
|
||||||
|
// Hash all existing edges to avoid creating duplicates.
|
||||||
|
edges := make(map[Edge]bool)
|
||||||
|
for _, cgn := range g.Nodes {
|
||||||
|
for _, e := range cgn.Out {
|
||||||
|
edges[*e] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for fn, cgn := range g.Nodes {
|
||||||
|
if cgn == g.Root || fn.Synthetic == "" || isInit(cgn.Func) {
|
||||||
|
continue // keep
|
||||||
|
}
|
||||||
|
for _, eIn := range cgn.In {
|
||||||
|
for _, eOut := range cgn.Out {
|
||||||
|
newEdge := Edge{eIn.Caller, eIn.Site, eOut.Callee}
|
||||||
|
if edges[newEdge] {
|
||||||
|
continue // don't add duplicate
|
||||||
|
}
|
||||||
|
AddEdge(eIn.Caller, eIn.Site, eOut.Callee)
|
||||||
|
edges[newEdge] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.DeleteNode(cgn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInit(fn *ssa.Function) bool {
|
||||||
|
return fn.Pkg != nil && fn.Pkg.Func("init") == fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNode removes node n and its edges from the graph g.
|
||||||
|
// (NB: not efficient for batch deletion.)
|
||||||
|
func (g *Graph) DeleteNode(n *Node) {
|
||||||
|
n.deleteIns()
|
||||||
|
n.deleteOuts()
|
||||||
|
delete(g.Nodes, n.Func)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteIns deletes all incoming edges to n.
|
||||||
|
func (n *Node) deleteIns() {
|
||||||
|
for _, e := range n.In {
|
||||||
|
removeOutEdge(e)
|
||||||
|
}
|
||||||
|
n.In = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteOuts deletes all outgoing edges from n.
|
||||||
|
func (n *Node) deleteOuts() {
|
||||||
|
for _, e := range n.Out {
|
||||||
|
removeInEdge(e)
|
||||||
|
}
|
||||||
|
n.Out = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeOutEdge removes edge.Caller's outgoing edge 'edge'.
|
||||||
|
func removeOutEdge(edge *Edge) {
|
||||||
|
caller := edge.Caller
|
||||||
|
n := len(caller.Out)
|
||||||
|
for i, e := range caller.Out {
|
||||||
|
if e == edge {
|
||||||
|
// Replace it with the final element and shrink the slice.
|
||||||
|
caller.Out[i] = caller.Out[n-1]
|
||||||
|
caller.Out[n-1] = nil // aid GC
|
||||||
|
caller.Out = caller.Out[:n-1]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("edge not found: " + edge.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeInEdge removes edge.Callee's incoming edge 'edge'.
|
||||||
|
func removeInEdge(edge *Edge) {
|
||||||
|
caller := edge.Callee
|
||||||
|
n := len(caller.In)
|
||||||
|
for i, e := range caller.In {
|
||||||
|
if e == edge {
|
||||||
|
// Replace it with the final element and shrink the slice.
|
||||||
|
caller.In[i] = caller.In[n-1]
|
||||||
|
caller.In[n-1] = nil // aid GC
|
||||||
|
caller.In = caller.In[:n-1]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("edge not found: " + edge.String())
|
||||||
|
}
|
16
vendor/honnef.co/go/tools/cmd/staticcheck/README.md
vendored
Normal file
16
vendor/honnef.co/go/tools/cmd/staticcheck/README.md
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# staticcheck
|
||||||
|
|
||||||
|
_staticcheck_ is `go vet` on steroids, applying a ton of static analysis
|
||||||
|
checks you might be used to from tools like ReSharper for C#.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Staticcheck requires Go 1.6 or later.
|
||||||
|
|
||||||
|
go get honnef.co/go/tools/cmd/staticcheck
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Detailed documentation can be found on
|
||||||
|
[staticcheck.io](https://staticcheck.io/docs/staticcheck).
|
||||||
|
|
23
vendor/honnef.co/go/tools/cmd/staticcheck/staticcheck.go
vendored
Normal file
23
vendor/honnef.co/go/tools/cmd/staticcheck/staticcheck.go
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// staticcheck detects a myriad of bugs and inefficiencies in your
|
||||||
|
// code.
|
||||||
|
package main // import "honnef.co/go/tools/cmd/staticcheck"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/lint/lintutil"
|
||||||
|
"honnef.co/go/tools/staticcheck"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fs := lintutil.FlagSet("staticcheck")
|
||||||
|
gen := fs.Bool("generated", false, "Check generated code")
|
||||||
|
fs.Parse(os.Args[1:])
|
||||||
|
c := staticcheck.NewChecker()
|
||||||
|
c.CheckGenerated = *gen
|
||||||
|
cfg := lintutil.CheckerConfig{
|
||||||
|
Checker: c,
|
||||||
|
ExitNonZero: true,
|
||||||
|
}
|
||||||
|
lintutil.ProcessFlagSet([]lintutil.CheckerConfig{cfg}, fs)
|
||||||
|
}
|
54
vendor/honnef.co/go/tools/deprecated/stdlib.go
vendored
Normal file
54
vendor/honnef.co/go/tools/deprecated/stdlib.go
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package deprecated
|
||||||
|
|
||||||
|
type Deprecation struct {
|
||||||
|
DeprecatedSince int
|
||||||
|
AlternativeAvailableSince int
|
||||||
|
}
|
||||||
|
|
||||||
|
var Stdlib = map[string]Deprecation{
|
||||||
|
"image/jpeg.Reader": {4, 0},
|
||||||
|
// FIXME(dh): AllowBinary isn't being detected as deprecated
|
||||||
|
// because the comment has a newline right after "Deprecated:"
|
||||||
|
"go/build.AllowBinary": {7, 7},
|
||||||
|
"(archive/zip.FileHeader).CompressedSize": {1, 1},
|
||||||
|
"(archive/zip.FileHeader).UncompressedSize": {1, 1},
|
||||||
|
"(go/doc.Package).Bugs": {1, 1},
|
||||||
|
"os.SEEK_SET": {7, 7},
|
||||||
|
"os.SEEK_CUR": {7, 7},
|
||||||
|
"os.SEEK_END": {7, 7},
|
||||||
|
"(net.Dialer).Cancel": {7, 7},
|
||||||
|
"runtime.CPUProfile": {9, 0},
|
||||||
|
"compress/flate.ReadError": {6, 6},
|
||||||
|
"compress/flate.WriteError": {6, 6},
|
||||||
|
"path/filepath.HasPrefix": {0, 0},
|
||||||
|
"(net/http.Transport).Dial": {7, 7},
|
||||||
|
"(*net/http.Transport).CancelRequest": {6, 5},
|
||||||
|
"net/http.ErrWriteAfterFlush": {7, 0},
|
||||||
|
"net/http.ErrHeaderTooLong": {8, 0},
|
||||||
|
"net/http.ErrShortBody": {8, 0},
|
||||||
|
"net/http.ErrMissingContentLength": {8, 0},
|
||||||
|
"net/http/httputil.ErrPersistEOF": {0, 0},
|
||||||
|
"net/http/httputil.ErrClosed": {0, 0},
|
||||||
|
"net/http/httputil.ErrPipeline": {0, 0},
|
||||||
|
"net/http/httputil.ServerConn": {0, 0},
|
||||||
|
"net/http/httputil.NewServerConn": {0, 0},
|
||||||
|
"net/http/httputil.ClientConn": {0, 0},
|
||||||
|
"net/http/httputil.NewClientConn": {0, 0},
|
||||||
|
"net/http/httputil.NewProxyClientConn": {0, 0},
|
||||||
|
"(net/http.Request).Cancel": {7, 7},
|
||||||
|
"(text/template/parse.PipeNode).Line": {1, 1},
|
||||||
|
"(text/template/parse.ActionNode).Line": {1, 1},
|
||||||
|
"(text/template/parse.BranchNode).Line": {1, 1},
|
||||||
|
"(text/template/parse.TemplateNode).Line": {1, 1},
|
||||||
|
"database/sql/driver.ColumnConverter": {9, 9},
|
||||||
|
"database/sql/driver.Execer": {8, 8},
|
||||||
|
"database/sql/driver.Queryer": {8, 8},
|
||||||
|
"(database/sql/driver.Conn).Begin": {8, 8},
|
||||||
|
"(database/sql/driver.Stmt).Exec": {8, 8},
|
||||||
|
"(database/sql/driver.Stmt).Query": {8, 8},
|
||||||
|
"syscall.StringByteSlice": {1, 1},
|
||||||
|
"syscall.StringBytePtr": {1, 1},
|
||||||
|
"syscall.StringSlicePtr": {1, 1},
|
||||||
|
"syscall.StringToUTF16": {1, 1},
|
||||||
|
"syscall.StringToUTF16Ptr": {1, 1},
|
||||||
|
}
|
56
vendor/honnef.co/go/tools/functions/concrete.go
vendored
Normal file
56
vendor/honnef.co/go/tools/functions/concrete.go
vendored
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
func concreteReturnTypes(fn *ssa.Function) []*types.Tuple {
|
||||||
|
res := fn.Signature.Results()
|
||||||
|
if res == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ifaces := make([]bool, res.Len())
|
||||||
|
any := false
|
||||||
|
for i := 0; i < res.Len(); i++ {
|
||||||
|
_, ifaces[i] = res.At(i).Type().Underlying().(*types.Interface)
|
||||||
|
any = any || ifaces[i]
|
||||||
|
}
|
||||||
|
if !any {
|
||||||
|
return []*types.Tuple{res}
|
||||||
|
}
|
||||||
|
var out []*types.Tuple
|
||||||
|
for _, block := range fn.Blocks {
|
||||||
|
if len(block.Instrs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret, ok := block.Instrs[len(block.Instrs)-1].(*ssa.Return)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vars := make([]*types.Var, res.Len())
|
||||||
|
for i, v := range ret.Results {
|
||||||
|
var typ types.Type
|
||||||
|
if !ifaces[i] {
|
||||||
|
typ = res.At(i).Type()
|
||||||
|
} else if mi, ok := v.(*ssa.MakeInterface); ok {
|
||||||
|
// TODO(dh): if mi.X is a function call that returns
|
||||||
|
// an interface, call concreteReturnTypes on that
|
||||||
|
// function (or, really, go through Descriptions,
|
||||||
|
// avoid infinite recursion etc, just like nil error
|
||||||
|
// detection)
|
||||||
|
|
||||||
|
// TODO(dh): support Phi nodes
|
||||||
|
typ = mi.X.Type()
|
||||||
|
} else {
|
||||||
|
typ = res.At(i).Type()
|
||||||
|
}
|
||||||
|
vars[i] = types.NewParam(token.NoPos, nil, "", typ)
|
||||||
|
}
|
||||||
|
out = append(out, types.NewTuple(vars...))
|
||||||
|
}
|
||||||
|
// TODO(dh): deduplicate out
|
||||||
|
return out
|
||||||
|
}
|
150
vendor/honnef.co/go/tools/functions/functions.go
vendored
Normal file
150
vendor/honnef.co/go/tools/functions/functions.go
vendored
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/types"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/callgraph"
|
||||||
|
"honnef.co/go/tools/callgraph/static"
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
"honnef.co/go/tools/staticcheck/vrp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stdlibDescs = map[string]Description{
|
||||||
|
"errors.New": {Pure: true},
|
||||||
|
|
||||||
|
"fmt.Errorf": {Pure: true},
|
||||||
|
"fmt.Sprintf": {Pure: true},
|
||||||
|
"fmt.Sprint": {Pure: true},
|
||||||
|
|
||||||
|
"sort.Reverse": {Pure: true},
|
||||||
|
|
||||||
|
"strings.Map": {Pure: true},
|
||||||
|
"strings.Repeat": {Pure: true},
|
||||||
|
"strings.Replace": {Pure: true},
|
||||||
|
"strings.Title": {Pure: true},
|
||||||
|
"strings.ToLower": {Pure: true},
|
||||||
|
"strings.ToLowerSpecial": {Pure: true},
|
||||||
|
"strings.ToTitle": {Pure: true},
|
||||||
|
"strings.ToTitleSpecial": {Pure: true},
|
||||||
|
"strings.ToUpper": {Pure: true},
|
||||||
|
"strings.ToUpperSpecial": {Pure: true},
|
||||||
|
"strings.Trim": {Pure: true},
|
||||||
|
"strings.TrimFunc": {Pure: true},
|
||||||
|
"strings.TrimLeft": {Pure: true},
|
||||||
|
"strings.TrimLeftFunc": {Pure: true},
|
||||||
|
"strings.TrimPrefix": {Pure: true},
|
||||||
|
"strings.TrimRight": {Pure: true},
|
||||||
|
"strings.TrimRightFunc": {Pure: true},
|
||||||
|
"strings.TrimSpace": {Pure: true},
|
||||||
|
"strings.TrimSuffix": {Pure: true},
|
||||||
|
|
||||||
|
"(*net/http.Request).WithContext": {Pure: true},
|
||||||
|
|
||||||
|
"math/rand.Read": {NilError: true},
|
||||||
|
"(*math/rand.Rand).Read": {NilError: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Description struct {
|
||||||
|
// The function is known to be pure
|
||||||
|
Pure bool
|
||||||
|
// The function is known to be a stub
|
||||||
|
Stub bool
|
||||||
|
// The function is known to never return (panics notwithstanding)
|
||||||
|
Infinite bool
|
||||||
|
// Variable ranges
|
||||||
|
Ranges vrp.Ranges
|
||||||
|
Loops []Loop
|
||||||
|
// Function returns an error as its last argument, but it is
|
||||||
|
// always nil
|
||||||
|
NilError bool
|
||||||
|
ConcreteReturnTypes []*types.Tuple
|
||||||
|
}
|
||||||
|
|
||||||
|
type descriptionEntry struct {
|
||||||
|
ready chan struct{}
|
||||||
|
result Description
|
||||||
|
}
|
||||||
|
|
||||||
|
type Descriptions struct {
|
||||||
|
CallGraph *callgraph.Graph
|
||||||
|
mu sync.Mutex
|
||||||
|
cache map[*ssa.Function]*descriptionEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDescriptions(prog *ssa.Program) *Descriptions {
|
||||||
|
return &Descriptions{
|
||||||
|
CallGraph: static.CallGraph(prog),
|
||||||
|
cache: map[*ssa.Function]*descriptionEntry{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Descriptions) Get(fn *ssa.Function) Description {
|
||||||
|
d.mu.Lock()
|
||||||
|
fd := d.cache[fn]
|
||||||
|
if fd == nil {
|
||||||
|
fd = &descriptionEntry{
|
||||||
|
ready: make(chan struct{}),
|
||||||
|
}
|
||||||
|
d.cache[fn] = fd
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
{
|
||||||
|
fd.result = stdlibDescs[fn.RelString(nil)]
|
||||||
|
fd.result.Pure = fd.result.Pure || d.IsPure(fn)
|
||||||
|
fd.result.Stub = fd.result.Stub || d.IsStub(fn)
|
||||||
|
fd.result.Infinite = fd.result.Infinite || !terminates(fn)
|
||||||
|
fd.result.Ranges = vrp.BuildGraph(fn).Solve()
|
||||||
|
fd.result.Loops = findLoops(fn)
|
||||||
|
fd.result.NilError = fd.result.NilError || IsNilError(fn)
|
||||||
|
fd.result.ConcreteReturnTypes = concreteReturnTypes(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
close(fd.ready)
|
||||||
|
} else {
|
||||||
|
d.mu.Unlock()
|
||||||
|
<-fd.ready
|
||||||
|
}
|
||||||
|
return fd.result
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNilError(fn *ssa.Function) bool {
|
||||||
|
// TODO(dh): This is very simplistic, as we only look for constant
|
||||||
|
// nil returns. A more advanced approach would work transitively.
|
||||||
|
// An even more advanced approach would be context-aware and
|
||||||
|
// determine nil errors based on inputs (e.g. io.WriteString to a
|
||||||
|
// bytes.Buffer will always return nil, but an io.WriteString to
|
||||||
|
// an os.File might not). Similarly, an os.File opened for reading
|
||||||
|
// won't error on Close, but other files will.
|
||||||
|
res := fn.Signature.Results()
|
||||||
|
if res.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
last := res.At(res.Len() - 1)
|
||||||
|
if types.TypeString(last.Type(), nil) != "error" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn.Blocks == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, block := range fn.Blocks {
|
||||||
|
if len(block.Instrs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ins := block.Instrs[len(block.Instrs)-1]
|
||||||
|
ret, ok := ins.(*ssa.Return)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v := ret.Results[len(ret.Results)-1]
|
||||||
|
c, ok := v.(*ssa.Const)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !c.IsNil() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
50
vendor/honnef.co/go/tools/functions/loops.go
vendored
Normal file
50
vendor/honnef.co/go/tools/functions/loops.go
vendored
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package functions
|
||||||
|
|
||||||
|
import "honnef.co/go/tools/ssa"
|
||||||
|
|
||||||
|
type Loop map[*ssa.BasicBlock]bool
|
||||||
|
|
||||||
|
func findLoops(fn *ssa.Function) []Loop {
|
||||||
|
if fn.Blocks == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tree := fn.DomPreorder()
|
||||||
|
var sets []Loop
|
||||||
|
for _, h := range tree {
|
||||||
|
for _, n := range h.Preds {
|
||||||
|
if !h.Dominates(n) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// n is a back-edge to h
|
||||||
|
// h is the loop header
|
||||||
|
if n == h {
|
||||||
|
sets = append(sets, Loop{n: true})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
set := Loop{h: true, n: true}
|
||||||
|
for _, b := range allPredsBut(n, h, nil) {
|
||||||
|
set[b] = true
|
||||||
|
}
|
||||||
|
sets = append(sets, set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
func allPredsBut(b, but *ssa.BasicBlock, list []*ssa.BasicBlock) []*ssa.BasicBlock {
|
||||||
|
outer:
|
||||||
|
for _, pred := range b.Preds {
|
||||||
|
if pred == but {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, p := range list {
|
||||||
|
// TODO improve big-o complexity of this function
|
||||||
|
if pred == p {
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list = append(list, pred)
|
||||||
|
list = allPredsBut(pred, but, list)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
123
vendor/honnef.co/go/tools/functions/pure.go
vendored
Normal file
123
vendor/honnef.co/go/tools/functions/pure.go
vendored
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package functions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/callgraph"
|
||||||
|
"honnef.co/go/tools/lint/lintdsl"
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsStub reports whether a function is a stub. A function is
|
||||||
|
// considered a stub if it has no instructions or exactly one
|
||||||
|
// instruction, which must be either returning only constant values or
|
||||||
|
// a panic.
|
||||||
|
func (d *Descriptions) IsStub(fn *ssa.Function) bool {
|
||||||
|
if len(fn.Blocks) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(fn.Blocks) > 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
instrs := lintdsl.FilterDebug(fn.Blocks[0].Instrs)
|
||||||
|
if len(instrs) != 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch instrs[0].(type) {
|
||||||
|
case *ssa.Return:
|
||||||
|
// Since this is the only instruction, the return value must
|
||||||
|
// be a constant. We consider all constants as stubs, not just
|
||||||
|
// the zero value. This does not, unfortunately, cover zero
|
||||||
|
// initialised structs, as these cause additional
|
||||||
|
// instructions.
|
||||||
|
return true
|
||||||
|
case *ssa.Panic:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Descriptions) IsPure(fn *ssa.Function) bool {
|
||||||
|
if fn.Signature.Results().Len() == 0 {
|
||||||
|
// A function with no return values is empty or is doing some
|
||||||
|
// work we cannot see (for example because of build tags);
|
||||||
|
// don't consider it pure.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, param := range fn.Params {
|
||||||
|
if _, ok := param.Type().Underlying().(*types.Basic); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn.Blocks == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
checkCall := func(common *ssa.CallCommon) bool {
|
||||||
|
if common.IsInvoke() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
builtin, ok := common.Value.(*ssa.Builtin)
|
||||||
|
if !ok {
|
||||||
|
if common.StaticCallee() != fn {
|
||||||
|
if common.StaticCallee() == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// TODO(dh): ideally, IsPure wouldn't be responsible
|
||||||
|
// for avoiding infinite recursion, but
|
||||||
|
// FunctionDescriptions would be.
|
||||||
|
node := d.CallGraph.CreateNode(common.StaticCallee())
|
||||||
|
if callgraph.PathSearch(node, func(other *callgraph.Node) bool {
|
||||||
|
return other.Func == fn
|
||||||
|
}) != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !d.Get(common.StaticCallee()).Pure {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch builtin.Name() {
|
||||||
|
case "len", "cap", "make", "new":
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, b := range fn.Blocks {
|
||||||
|
for _, ins := range b.Instrs {
|
||||||
|
switch ins := ins.(type) {
|
||||||
|
case *ssa.Call:
|
||||||
|
if !checkCall(ins.Common()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case *ssa.Defer:
|
||||||
|
if !checkCall(&ins.Call) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case *ssa.Select:
|
||||||
|
return false
|
||||||
|
case *ssa.Send:
|
||||||
|
return false
|
||||||
|
case *ssa.Go:
|
||||||
|
return false
|
||||||
|
case *ssa.Panic:
|
||||||
|
return false
|
||||||
|
case *ssa.Store:
|
||||||
|
return false
|
||||||
|
case *ssa.FieldAddr:
|
||||||
|
return false
|
||||||
|
case *ssa.UnOp:
|
||||||
|
if ins.Op == token.MUL || ins.Op == token.AND {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
24
vendor/honnef.co/go/tools/functions/terminates.go
vendored
Normal file
24
vendor/honnef.co/go/tools/functions/terminates.go
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package functions
|
||||||
|
|
||||||
|
import "honnef.co/go/tools/ssa"
|
||||||
|
|
||||||
|
// terminates reports whether fn is supposed to return, that is if it
|
||||||
|
// has at least one theoretic path that returns from the function.
|
||||||
|
// Explicit panics do not count as terminating.
|
||||||
|
func terminates(fn *ssa.Function) bool {
|
||||||
|
if fn.Blocks == nil {
|
||||||
|
// assuming that a function terminates is the conservative
|
||||||
|
// choice
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, block := range fn.Blocks {
|
||||||
|
if len(block.Instrs) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := block.Instrs[len(block.Instrs)-1].(*ssa.Return); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
15
vendor/honnef.co/go/tools/staticcheck/CONTRIBUTING.md
vendored
Normal file
15
vendor/honnef.co/go/tools/staticcheck/CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Contributing to staticcheck
|
||||||
|
|
||||||
|
## Before filing an issue:
|
||||||
|
|
||||||
|
### Are you having trouble building staticcheck?
|
||||||
|
|
||||||
|
Check you have the latest version of its dependencies. Run
|
||||||
|
```
|
||||||
|
go get -u honnef.co/go/tools/staticcheck
|
||||||
|
```
|
||||||
|
If you still have problems, consider searching for existing issues before filing a new issue.
|
||||||
|
|
||||||
|
## Before sending a pull request:
|
||||||
|
|
||||||
|
Have you understood the purpose of staticcheck? Make sure to carefully read `README`.
|
21
vendor/honnef.co/go/tools/staticcheck/buildtag.go
vendored
Normal file
21
vendor/honnef.co/go/tools/staticcheck/buildtag.go
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package staticcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/ast"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "honnef.co/go/tools/lint/lintdsl"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildTags(f *ast.File) [][]string {
|
||||||
|
var out [][]string
|
||||||
|
for _, line := range strings.Split(Preamble(f), "\n") {
|
||||||
|
if !strings.HasPrefix(line, "+build ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(strings.TrimPrefix(line, "+build "))
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
out = append(out, fields)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
2790
vendor/honnef.co/go/tools/staticcheck/lint.go
vendored
Normal file
2790
vendor/honnef.co/go/tools/staticcheck/lint.go
vendored
Normal file
File diff suppressed because it is too large
Load diff
322
vendor/honnef.co/go/tools/staticcheck/rules.go
vendored
Normal file
322
vendor/honnef.co/go/tools/staticcheck/rules.go
vendored
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
package staticcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/constant"
|
||||||
|
"go/types"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/lint"
|
||||||
|
. "honnef.co/go/tools/lint/lintdsl"
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
"honnef.co/go/tools/staticcheck/vrp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MsgInvalidHostPort = "invalid port or service name in host:port pair"
|
||||||
|
MsgInvalidUTF8 = "argument is not a valid UTF-8 encoded string"
|
||||||
|
MsgNonUniqueCutset = "cutset contains duplicate characters"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Call struct {
|
||||||
|
Job *lint.Job
|
||||||
|
Instr ssa.CallInstruction
|
||||||
|
Args []*Argument
|
||||||
|
|
||||||
|
Checker *Checker
|
||||||
|
Parent *ssa.Function
|
||||||
|
|
||||||
|
invalids []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Call) Invalid(msg string) {
|
||||||
|
c.invalids = append(c.invalids, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Argument struct {
|
||||||
|
Value Value
|
||||||
|
invalids []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (arg *Argument) Invalid(msg string) {
|
||||||
|
arg.invalids = append(arg.invalids, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Value struct {
|
||||||
|
Value ssa.Value
|
||||||
|
Range vrp.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallCheck func(call *Call)
|
||||||
|
|
||||||
|
func extractConsts(v ssa.Value) []*ssa.Const {
|
||||||
|
switch v := v.(type) {
|
||||||
|
case *ssa.Const:
|
||||||
|
return []*ssa.Const{v}
|
||||||
|
case *ssa.MakeInterface:
|
||||||
|
return extractConsts(v.X)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateRegexp(v Value) error {
|
||||||
|
for _, c := range extractConsts(v.Value) {
|
||||||
|
if c.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Value.Kind() != constant.String {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := constant.StringVal(c.Value)
|
||||||
|
if _, err := regexp.Compile(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateTimeLayout(v Value) error {
|
||||||
|
for _, c := range extractConsts(v.Value) {
|
||||||
|
if c.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Value.Kind() != constant.String {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := constant.StringVal(c.Value)
|
||||||
|
s = strings.Replace(s, "_", " ", -1)
|
||||||
|
s = strings.Replace(s, "Z", "-", -1)
|
||||||
|
_, err := time.Parse(s, s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateURL(v Value) error {
|
||||||
|
for _, c := range extractConsts(v.Value) {
|
||||||
|
if c.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Value.Kind() != constant.String {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := constant.StringVal(c.Value)
|
||||||
|
_, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%q is not a valid URL: %s", s, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntValue(v Value, z vrp.Z) bool {
|
||||||
|
r, ok := v.Range.(vrp.IntInterval)
|
||||||
|
if !ok || !r.IsKnown() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.Lower != r.Upper {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.Lower.Cmp(z) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func InvalidUTF8(v Value) bool {
|
||||||
|
for _, c := range extractConsts(v.Value) {
|
||||||
|
if c.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Value.Kind() != constant.String {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := constant.StringVal(c.Value)
|
||||||
|
if !utf8.ValidString(s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnbufferedChannel(v Value) bool {
|
||||||
|
r, ok := v.Range.(vrp.ChannelInterval)
|
||||||
|
if !ok || !r.IsKnown() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.Size.Lower.Cmp(vrp.NewZ(0)) == 0 &&
|
||||||
|
r.Size.Upper.Cmp(vrp.NewZ(0)) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pointer(v Value) bool {
|
||||||
|
switch v.Value.Type().Underlying().(type) {
|
||||||
|
case *types.Pointer, *types.Interface:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertedFromInt(v Value) bool {
|
||||||
|
conv, ok := v.Value.(*ssa.Convert)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b, ok := conv.X.Type().Underlying().(*types.Basic)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (b.Info() & types.IsInteger) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func validEncodingBinaryType(j *lint.Job, typ types.Type) bool {
|
||||||
|
typ = typ.Underlying()
|
||||||
|
switch typ := typ.(type) {
|
||||||
|
case *types.Basic:
|
||||||
|
switch typ.Kind() {
|
||||||
|
case types.Uint8, types.Uint16, types.Uint32, types.Uint64,
|
||||||
|
types.Int8, types.Int16, types.Int32, types.Int64,
|
||||||
|
types.Float32, types.Float64, types.Complex64, types.Complex128, types.Invalid:
|
||||||
|
return true
|
||||||
|
case types.Bool:
|
||||||
|
return IsGoVersion(j, 8)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
case *types.Struct:
|
||||||
|
n := typ.NumFields()
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if !validEncodingBinaryType(j, typ.Field(i).Type()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case *types.Array:
|
||||||
|
return validEncodingBinaryType(j, typ.Elem())
|
||||||
|
case *types.Interface:
|
||||||
|
// we can't determine if it's a valid type or not
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CanBinaryMarshal(j *lint.Job, v Value) bool {
|
||||||
|
typ := v.Value.Type().Underlying()
|
||||||
|
if ttyp, ok := typ.(*types.Pointer); ok {
|
||||||
|
typ = ttyp.Elem().Underlying()
|
||||||
|
}
|
||||||
|
if ttyp, ok := typ.(interface {
|
||||||
|
Elem() types.Type
|
||||||
|
}); ok {
|
||||||
|
if _, ok := ttyp.(*types.Pointer); !ok {
|
||||||
|
typ = ttyp.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validEncodingBinaryType(j, typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepeatZeroTimes(name string, arg int) CallCheck {
|
||||||
|
return func(call *Call) {
|
||||||
|
arg := call.Args[arg]
|
||||||
|
if IntValue(arg.Value, vrp.NewZ(0)) {
|
||||||
|
arg.Invalid(fmt.Sprintf("calling %s with n == 0 will return no results, did you mean -1?", name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateServiceName(s string) bool {
|
||||||
|
if len(s) < 1 || len(s) > 15 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s[0] == '-' || s[len(s)-1] == '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "--") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hasLetter := false
|
||||||
|
for _, r := range s {
|
||||||
|
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
|
||||||
|
hasLetter = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r >= '0' && r <= '9' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasLetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePort(s string) bool {
|
||||||
|
n, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return validateServiceName(s)
|
||||||
|
}
|
||||||
|
return n >= 0 && n <= 65535
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidHostPort(v Value) bool {
|
||||||
|
for _, k := range extractConsts(v.Value) {
|
||||||
|
if k.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if k.Value.Kind() != constant.String {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := constant.StringVal(k.Value)
|
||||||
|
_, port, err := net.SplitHostPort(s)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// TODO(dh): check hostname
|
||||||
|
if !validatePort(port) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertedFrom reports whether value v was converted from type typ.
|
||||||
|
func ConvertedFrom(v Value, typ string) bool {
|
||||||
|
change, ok := v.Value.(*ssa.ChangeType)
|
||||||
|
return ok && IsType(change.X.Type(), typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UniqueStringCutset(v Value) bool {
|
||||||
|
for _, c := range extractConsts(v.Value) {
|
||||||
|
if c.Value == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Value.Kind() != constant.String {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := constant.StringVal(c.Value)
|
||||||
|
rs := runeSlice(s)
|
||||||
|
if len(rs) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sort.Sort(rs)
|
||||||
|
for i, r := range rs[1:] {
|
||||||
|
if rs[i] == r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
73
vendor/honnef.co/go/tools/staticcheck/vrp/channel.go
vendored
Normal file
73
vendor/honnef.co/go/tools/staticcheck/vrp/channel.go
vendored
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package vrp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChannelInterval struct {
|
||||||
|
Size IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChannelInterval) Union(other Range) Range {
|
||||||
|
i, ok := other.(ChannelInterval)
|
||||||
|
if !ok {
|
||||||
|
i = ChannelInterval{EmptyIntInterval}
|
||||||
|
}
|
||||||
|
if c.Size.Empty() || !c.Size.IsKnown() {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
if i.Size.Empty() || !i.Size.IsKnown() {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return ChannelInterval{
|
||||||
|
Size: c.Size.Union(i.Size).(IntInterval),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChannelInterval) String() string {
|
||||||
|
return c.Size.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c ChannelInterval) IsKnown() bool {
|
||||||
|
return c.Size.IsKnown()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MakeChannelConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
Buffer ssa.Value
|
||||||
|
}
|
||||||
|
type ChannelChangeTypeConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMakeChannelConstraint(buffer, y ssa.Value) Constraint {
|
||||||
|
return &MakeChannelConstraint{NewConstraint(y), buffer}
|
||||||
|
}
|
||||||
|
func NewChannelChangeTypeConstraint(x, y ssa.Value) Constraint {
|
||||||
|
return &ChannelChangeTypeConstraint{NewConstraint(y), x}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MakeChannelConstraint) Operands() []ssa.Value { return []ssa.Value{c.Buffer} }
|
||||||
|
func (c *ChannelChangeTypeConstraint) Operands() []ssa.Value { return []ssa.Value{c.X} }
|
||||||
|
|
||||||
|
func (c *MakeChannelConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = make(chan, %s)", c.Y().Name(), c.Buffer.Name())
|
||||||
|
}
|
||||||
|
func (c *ChannelChangeTypeConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = changetype(%s)", c.Y().Name(), c.X.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MakeChannelConstraint) Eval(g *Graph) Range {
|
||||||
|
i, ok := g.Range(c.Buffer).(IntInterval)
|
||||||
|
if !ok {
|
||||||
|
return ChannelInterval{NewIntInterval(NewZ(0), PInfinity)}
|
||||||
|
}
|
||||||
|
if i.Lower.Sign() == -1 {
|
||||||
|
i.Lower = NewZ(0)
|
||||||
|
}
|
||||||
|
return ChannelInterval{i}
|
||||||
|
}
|
||||||
|
func (c *ChannelChangeTypeConstraint) Eval(g *Graph) Range { return g.Range(c.X) }
|
476
vendor/honnef.co/go/tools/staticcheck/vrp/int.go
vendored
Normal file
476
vendor/honnef.co/go/tools/staticcheck/vrp/int.go
vendored
Normal file
|
@ -0,0 +1,476 @@
|
||||||
|
package vrp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Zs []Z
|
||||||
|
|
||||||
|
func (zs Zs) Len() int {
|
||||||
|
return len(zs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zs Zs) Less(i int, j int) bool {
|
||||||
|
return zs[i].Cmp(zs[j]) == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zs Zs) Swap(i int, j int) {
|
||||||
|
zs[i], zs[j] = zs[j], zs[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z struct {
|
||||||
|
infinity int8
|
||||||
|
integer *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZ(n int64) Z {
|
||||||
|
return NewBigZ(big.NewInt(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBigZ(n *big.Int) Z {
|
||||||
|
return Z{integer: n}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) Infinite() bool {
|
||||||
|
return z1.infinity != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) Add(z2 Z) Z {
|
||||||
|
if z2.Sign() == -1 {
|
||||||
|
return z1.Sub(z2.Negate())
|
||||||
|
}
|
||||||
|
if z1 == NInfinity {
|
||||||
|
return NInfinity
|
||||||
|
}
|
||||||
|
if z1 == PInfinity {
|
||||||
|
return PInfinity
|
||||||
|
}
|
||||||
|
if z2 == PInfinity {
|
||||||
|
return PInfinity
|
||||||
|
}
|
||||||
|
|
||||||
|
if !z1.Infinite() && !z2.Infinite() {
|
||||||
|
n := &big.Int{}
|
||||||
|
n.Add(z1.integer, z2.integer)
|
||||||
|
return NewBigZ(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("%s + %s is not defined", z1, z2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) Sub(z2 Z) Z {
|
||||||
|
if z2.Sign() == -1 {
|
||||||
|
return z1.Add(z2.Negate())
|
||||||
|
}
|
||||||
|
if !z1.Infinite() && !z2.Infinite() {
|
||||||
|
n := &big.Int{}
|
||||||
|
n.Sub(z1.integer, z2.integer)
|
||||||
|
return NewBigZ(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if z1 != PInfinity && z2 == PInfinity {
|
||||||
|
return NInfinity
|
||||||
|
}
|
||||||
|
if z1.Infinite() && !z2.Infinite() {
|
||||||
|
return Z{infinity: z1.infinity}
|
||||||
|
}
|
||||||
|
if z1 == PInfinity && z2 == PInfinity {
|
||||||
|
return PInfinity
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("%s - %s is not defined", z1, z2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) Mul(z2 Z) Z {
|
||||||
|
if (z1.integer != nil && z1.integer.Sign() == 0) ||
|
||||||
|
(z2.integer != nil && z2.integer.Sign() == 0) {
|
||||||
|
return NewBigZ(&big.Int{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if z1.infinity != 0 || z2.infinity != 0 {
|
||||||
|
return Z{infinity: int8(z1.Sign() * z2.Sign())}
|
||||||
|
}
|
||||||
|
|
||||||
|
n := &big.Int{}
|
||||||
|
n.Mul(z1.integer, z2.integer)
|
||||||
|
return NewBigZ(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) Negate() Z {
|
||||||
|
if z1.infinity == 1 {
|
||||||
|
return NInfinity
|
||||||
|
}
|
||||||
|
if z1.infinity == -1 {
|
||||||
|
return PInfinity
|
||||||
|
}
|
||||||
|
n := &big.Int{}
|
||||||
|
n.Neg(z1.integer)
|
||||||
|
return NewBigZ(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) Sign() int {
|
||||||
|
if z1.infinity != 0 {
|
||||||
|
return int(z1.infinity)
|
||||||
|
}
|
||||||
|
return z1.integer.Sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) String() string {
|
||||||
|
if z1 == NInfinity {
|
||||||
|
return "-∞"
|
||||||
|
}
|
||||||
|
if z1 == PInfinity {
|
||||||
|
return "∞"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", z1.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z1 Z) Cmp(z2 Z) int {
|
||||||
|
if z1.infinity == z2.infinity && z1.infinity != 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if z1 == PInfinity {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if z1 == NInfinity {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if z2 == NInfinity {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if z2 == PInfinity {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return z1.integer.Cmp(z2.integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MaxZ(zs ...Z) Z {
|
||||||
|
if len(zs) == 0 {
|
||||||
|
panic("Max called with no arguments")
|
||||||
|
}
|
||||||
|
if len(zs) == 1 {
|
||||||
|
return zs[0]
|
||||||
|
}
|
||||||
|
ret := zs[0]
|
||||||
|
for _, z := range zs[1:] {
|
||||||
|
if z.Cmp(ret) == 1 {
|
||||||
|
ret = z
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func MinZ(zs ...Z) Z {
|
||||||
|
if len(zs) == 0 {
|
||||||
|
panic("Min called with no arguments")
|
||||||
|
}
|
||||||
|
if len(zs) == 1 {
|
||||||
|
return zs[0]
|
||||||
|
}
|
||||||
|
ret := zs[0]
|
||||||
|
for _, z := range zs[1:] {
|
||||||
|
if z.Cmp(ret) == -1 {
|
||||||
|
ret = z
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
var NInfinity = Z{infinity: -1}
|
||||||
|
var PInfinity = Z{infinity: 1}
|
||||||
|
var EmptyIntInterval = IntInterval{true, PInfinity, NInfinity}
|
||||||
|
|
||||||
|
func InfinityFor(v ssa.Value) IntInterval {
|
||||||
|
if b, ok := v.Type().Underlying().(*types.Basic); ok {
|
||||||
|
if (b.Info() & types.IsUnsigned) != 0 {
|
||||||
|
return NewIntInterval(NewZ(0), PInfinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NewIntInterval(NInfinity, PInfinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntInterval struct {
|
||||||
|
known bool
|
||||||
|
Lower Z
|
||||||
|
Upper Z
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIntInterval(l, u Z) IntInterval {
|
||||||
|
if u.Cmp(l) == -1 {
|
||||||
|
return EmptyIntInterval
|
||||||
|
}
|
||||||
|
return IntInterval{known: true, Lower: l, Upper: u}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i IntInterval) IsKnown() bool {
|
||||||
|
return i.known
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i IntInterval) Empty() bool {
|
||||||
|
return i.Lower == PInfinity && i.Upper == NInfinity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i IntInterval) IsMaxRange() bool {
|
||||||
|
return i.Lower == NInfinity && i.Upper == PInfinity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i1 IntInterval) Intersection(i2 IntInterval) IntInterval {
|
||||||
|
if !i1.IsKnown() {
|
||||||
|
return i2
|
||||||
|
}
|
||||||
|
if !i2.IsKnown() {
|
||||||
|
return i1
|
||||||
|
}
|
||||||
|
if i1.Empty() || i2.Empty() {
|
||||||
|
return EmptyIntInterval
|
||||||
|
}
|
||||||
|
i3 := NewIntInterval(MaxZ(i1.Lower, i2.Lower), MinZ(i1.Upper, i2.Upper))
|
||||||
|
if i3.Lower.Cmp(i3.Upper) == 1 {
|
||||||
|
return EmptyIntInterval
|
||||||
|
}
|
||||||
|
return i3
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i1 IntInterval) Union(other Range) Range {
|
||||||
|
i2, ok := other.(IntInterval)
|
||||||
|
if !ok {
|
||||||
|
i2 = EmptyIntInterval
|
||||||
|
}
|
||||||
|
if i1.Empty() || !i1.IsKnown() {
|
||||||
|
return i2
|
||||||
|
}
|
||||||
|
if i2.Empty() || !i2.IsKnown() {
|
||||||
|
return i1
|
||||||
|
}
|
||||||
|
return NewIntInterval(MinZ(i1.Lower, i2.Lower), MaxZ(i1.Upper, i2.Upper))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i1 IntInterval) Add(i2 IntInterval) IntInterval {
|
||||||
|
if i1.Empty() || i2.Empty() {
|
||||||
|
return EmptyIntInterval
|
||||||
|
}
|
||||||
|
l1, u1, l2, u2 := i1.Lower, i1.Upper, i2.Lower, i2.Upper
|
||||||
|
return NewIntInterval(l1.Add(l2), u1.Add(u2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i1 IntInterval) Sub(i2 IntInterval) IntInterval {
|
||||||
|
if i1.Empty() || i2.Empty() {
|
||||||
|
return EmptyIntInterval
|
||||||
|
}
|
||||||
|
l1, u1, l2, u2 := i1.Lower, i1.Upper, i2.Lower, i2.Upper
|
||||||
|
return NewIntInterval(l1.Sub(u2), u1.Sub(l2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i1 IntInterval) Mul(i2 IntInterval) IntInterval {
|
||||||
|
if i1.Empty() || i2.Empty() {
|
||||||
|
return EmptyIntInterval
|
||||||
|
}
|
||||||
|
x1, x2 := i1.Lower, i1.Upper
|
||||||
|
y1, y2 := i2.Lower, i2.Upper
|
||||||
|
return NewIntInterval(
|
||||||
|
MinZ(x1.Mul(y1), x1.Mul(y2), x2.Mul(y1), x2.Mul(y2)),
|
||||||
|
MaxZ(x1.Mul(y1), x1.Mul(y2), x2.Mul(y1), x2.Mul(y2)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i1 IntInterval) String() string {
|
||||||
|
if !i1.IsKnown() {
|
||||||
|
return "[⊥, ⊥]"
|
||||||
|
}
|
||||||
|
if i1.Empty() {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s, %s]", i1.Lower, i1.Upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntArithmeticConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
A ssa.Value
|
||||||
|
B ssa.Value
|
||||||
|
Op token.Token
|
||||||
|
Fn func(IntInterval, IntInterval) IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntAddConstraint struct{ *IntArithmeticConstraint }
|
||||||
|
type IntSubConstraint struct{ *IntArithmeticConstraint }
|
||||||
|
type IntMulConstraint struct{ *IntArithmeticConstraint }
|
||||||
|
|
||||||
|
type IntConversionConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntIntersectionConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
ranges Ranges
|
||||||
|
A ssa.Value
|
||||||
|
B ssa.Value
|
||||||
|
Op token.Token
|
||||||
|
I IntInterval
|
||||||
|
resolved bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type IntIntervalConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
I IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIntArithmeticConstraint(a, b, y ssa.Value, op token.Token, fn func(IntInterval, IntInterval) IntInterval) *IntArithmeticConstraint {
|
||||||
|
return &IntArithmeticConstraint{NewConstraint(y), a, b, op, fn}
|
||||||
|
}
|
||||||
|
func NewIntAddConstraint(a, b, y ssa.Value) Constraint {
|
||||||
|
return &IntAddConstraint{NewIntArithmeticConstraint(a, b, y, token.ADD, IntInterval.Add)}
|
||||||
|
}
|
||||||
|
func NewIntSubConstraint(a, b, y ssa.Value) Constraint {
|
||||||
|
return &IntSubConstraint{NewIntArithmeticConstraint(a, b, y, token.SUB, IntInterval.Sub)}
|
||||||
|
}
|
||||||
|
func NewIntMulConstraint(a, b, y ssa.Value) Constraint {
|
||||||
|
return &IntMulConstraint{NewIntArithmeticConstraint(a, b, y, token.MUL, IntInterval.Mul)}
|
||||||
|
}
|
||||||
|
func NewIntConversionConstraint(x, y ssa.Value) Constraint {
|
||||||
|
return &IntConversionConstraint{NewConstraint(y), x}
|
||||||
|
}
|
||||||
|
func NewIntIntersectionConstraint(a, b ssa.Value, op token.Token, ranges Ranges, y ssa.Value) Constraint {
|
||||||
|
return &IntIntersectionConstraint{
|
||||||
|
aConstraint: NewConstraint(y),
|
||||||
|
ranges: ranges,
|
||||||
|
A: a,
|
||||||
|
B: b,
|
||||||
|
Op: op,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func NewIntIntervalConstraint(i IntInterval, y ssa.Value) Constraint {
|
||||||
|
return &IntIntervalConstraint{NewConstraint(y), i}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IntArithmeticConstraint) Operands() []ssa.Value { return []ssa.Value{c.A, c.B} }
|
||||||
|
func (c *IntConversionConstraint) Operands() []ssa.Value { return []ssa.Value{c.X} }
|
||||||
|
func (c *IntIntersectionConstraint) Operands() []ssa.Value { return []ssa.Value{c.A} }
|
||||||
|
func (s *IntIntervalConstraint) Operands() []ssa.Value { return nil }
|
||||||
|
|
||||||
|
func (c *IntArithmeticConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = %s %s %s", c.Y().Name(), c.A.Name(), c.Op, c.B.Name())
|
||||||
|
}
|
||||||
|
func (c *IntConversionConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = %s(%s)", c.Y().Name(), c.Y().Type(), c.X.Name())
|
||||||
|
}
|
||||||
|
func (c *IntIntersectionConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = %s %s %s (%t branch)", c.Y().Name(), c.A.Name(), c.Op, c.B.Name(), c.Y().(*ssa.Sigma).Branch)
|
||||||
|
}
|
||||||
|
func (c *IntIntervalConstraint) String() string { return fmt.Sprintf("%s = %s", c.Y().Name(), c.I) }
|
||||||
|
|
||||||
|
func (c *IntArithmeticConstraint) Eval(g *Graph) Range {
|
||||||
|
i1, i2 := g.Range(c.A).(IntInterval), g.Range(c.B).(IntInterval)
|
||||||
|
if !i1.IsKnown() || !i2.IsKnown() {
|
||||||
|
return IntInterval{}
|
||||||
|
}
|
||||||
|
return c.Fn(i1, i2)
|
||||||
|
}
|
||||||
|
func (c *IntConversionConstraint) Eval(g *Graph) Range {
|
||||||
|
s := &types.StdSizes{
|
||||||
|
// XXX is it okay to assume the largest word size, or do we
|
||||||
|
// need to be platform specific?
|
||||||
|
WordSize: 8,
|
||||||
|
MaxAlign: 1,
|
||||||
|
}
|
||||||
|
fromI := g.Range(c.X).(IntInterval)
|
||||||
|
toI := g.Range(c.Y()).(IntInterval)
|
||||||
|
fromT := c.X.Type().Underlying().(*types.Basic)
|
||||||
|
toT := c.Y().Type().Underlying().(*types.Basic)
|
||||||
|
fromB := s.Sizeof(c.X.Type())
|
||||||
|
toB := s.Sizeof(c.Y().Type())
|
||||||
|
|
||||||
|
if !fromI.IsKnown() {
|
||||||
|
return toI
|
||||||
|
}
|
||||||
|
if !toI.IsKnown() {
|
||||||
|
return fromI
|
||||||
|
}
|
||||||
|
|
||||||
|
// uint<N> -> sint/uint<M>, M > N: [max(0, l1), min(2**N-1, u2)]
|
||||||
|
if (fromT.Info()&types.IsUnsigned != 0) &&
|
||||||
|
toB > fromB {
|
||||||
|
|
||||||
|
n := big.NewInt(1)
|
||||||
|
n.Lsh(n, uint(fromB*8))
|
||||||
|
n.Sub(n, big.NewInt(1))
|
||||||
|
return NewIntInterval(
|
||||||
|
MaxZ(NewZ(0), fromI.Lower),
|
||||||
|
MinZ(NewBigZ(n), toI.Upper),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sint<N> -> sint<M>, M > N; [max(-∞, l1), min(2**N-1, u2)]
|
||||||
|
if (fromT.Info()&types.IsUnsigned == 0) &&
|
||||||
|
(toT.Info()&types.IsUnsigned == 0) &&
|
||||||
|
toB > fromB {
|
||||||
|
|
||||||
|
n := big.NewInt(1)
|
||||||
|
n.Lsh(n, uint(fromB*8))
|
||||||
|
n.Sub(n, big.NewInt(1))
|
||||||
|
return NewIntInterval(
|
||||||
|
MaxZ(NInfinity, fromI.Lower),
|
||||||
|
MinZ(NewBigZ(n), toI.Upper),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromI
|
||||||
|
}
|
||||||
|
func (c *IntIntersectionConstraint) Eval(g *Graph) Range {
|
||||||
|
xi := g.Range(c.A).(IntInterval)
|
||||||
|
if !xi.IsKnown() {
|
||||||
|
return c.I
|
||||||
|
}
|
||||||
|
return xi.Intersection(c.I)
|
||||||
|
}
|
||||||
|
func (c *IntIntervalConstraint) Eval(*Graph) Range { return c.I }
|
||||||
|
|
||||||
|
func (c *IntIntersectionConstraint) Futures() []ssa.Value {
|
||||||
|
return []ssa.Value{c.B}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IntIntersectionConstraint) Resolve() {
|
||||||
|
r, ok := c.ranges[c.B].(IntInterval)
|
||||||
|
if !ok {
|
||||||
|
c.I = InfinityFor(c.Y())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Op {
|
||||||
|
case token.EQL:
|
||||||
|
c.I = r
|
||||||
|
case token.GTR:
|
||||||
|
c.I = NewIntInterval(r.Lower.Add(NewZ(1)), PInfinity)
|
||||||
|
case token.GEQ:
|
||||||
|
c.I = NewIntInterval(r.Lower, PInfinity)
|
||||||
|
case token.LSS:
|
||||||
|
// TODO(dh): do we need 0 instead of NInfinity for uints?
|
||||||
|
c.I = NewIntInterval(NInfinity, r.Upper.Sub(NewZ(1)))
|
||||||
|
case token.LEQ:
|
||||||
|
c.I = NewIntInterval(NInfinity, r.Upper)
|
||||||
|
case token.NEQ:
|
||||||
|
c.I = InfinityFor(c.Y())
|
||||||
|
default:
|
||||||
|
panic("unsupported op " + c.Op.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IntIntersectionConstraint) IsKnown() bool {
|
||||||
|
return c.I.IsKnown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IntIntersectionConstraint) MarkUnresolved() {
|
||||||
|
c.resolved = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IntIntersectionConstraint) MarkResolved() {
|
||||||
|
c.resolved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IntIntersectionConstraint) IsResolved() bool {
|
||||||
|
return c.resolved
|
||||||
|
}
|
273
vendor/honnef.co/go/tools/staticcheck/vrp/slice.go
vendored
Normal file
273
vendor/honnef.co/go/tools/staticcheck/vrp/slice.go
vendored
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
package vrp
|
||||||
|
|
||||||
|
// TODO(dh): most of the constraints have implementations identical to
|
||||||
|
// that of strings. Consider reusing them.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/types"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SliceInterval struct {
|
||||||
|
Length IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SliceInterval) Union(other Range) Range {
|
||||||
|
i, ok := other.(SliceInterval)
|
||||||
|
if !ok {
|
||||||
|
i = SliceInterval{EmptyIntInterval}
|
||||||
|
}
|
||||||
|
if s.Length.Empty() || !s.Length.IsKnown() {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
if i.Length.Empty() || !i.Length.IsKnown() {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return SliceInterval{
|
||||||
|
Length: s.Length.Union(i.Length).(IntInterval),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s SliceInterval) String() string { return s.Length.String() }
|
||||||
|
func (s SliceInterval) IsKnown() bool { return s.Length.IsKnown() }
|
||||||
|
|
||||||
|
type SliceAppendConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
A ssa.Value
|
||||||
|
B ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type SliceSliceConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
Lower ssa.Value
|
||||||
|
Upper ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArraySliceConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
Lower ssa.Value
|
||||||
|
Upper ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type SliceIntersectionConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
I IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
type SliceLengthConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type MakeSliceConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
Size ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type SliceIntervalConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
I IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSliceAppendConstraint(a, b, y ssa.Value) Constraint {
|
||||||
|
return &SliceAppendConstraint{NewConstraint(y), a, b}
|
||||||
|
}
|
||||||
|
func NewSliceSliceConstraint(x, lower, upper, y ssa.Value) Constraint {
|
||||||
|
return &SliceSliceConstraint{NewConstraint(y), x, lower, upper}
|
||||||
|
}
|
||||||
|
func NewArraySliceConstraint(x, lower, upper, y ssa.Value) Constraint {
|
||||||
|
return &ArraySliceConstraint{NewConstraint(y), x, lower, upper}
|
||||||
|
}
|
||||||
|
func NewSliceIntersectionConstraint(x ssa.Value, i IntInterval, y ssa.Value) Constraint {
|
||||||
|
return &SliceIntersectionConstraint{NewConstraint(y), x, i}
|
||||||
|
}
|
||||||
|
func NewSliceLengthConstraint(x, y ssa.Value) Constraint {
|
||||||
|
return &SliceLengthConstraint{NewConstraint(y), x}
|
||||||
|
}
|
||||||
|
func NewMakeSliceConstraint(size, y ssa.Value) Constraint {
|
||||||
|
return &MakeSliceConstraint{NewConstraint(y), size}
|
||||||
|
}
|
||||||
|
func NewSliceIntervalConstraint(i IntInterval, y ssa.Value) Constraint {
|
||||||
|
return &SliceIntervalConstraint{NewConstraint(y), i}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SliceAppendConstraint) Operands() []ssa.Value { return []ssa.Value{c.A, c.B} }
|
||||||
|
func (c *SliceSliceConstraint) Operands() []ssa.Value {
|
||||||
|
ops := []ssa.Value{c.X}
|
||||||
|
if c.Lower != nil {
|
||||||
|
ops = append(ops, c.Lower)
|
||||||
|
}
|
||||||
|
if c.Upper != nil {
|
||||||
|
ops = append(ops, c.Upper)
|
||||||
|
}
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
func (c *ArraySliceConstraint) Operands() []ssa.Value {
|
||||||
|
ops := []ssa.Value{c.X}
|
||||||
|
if c.Lower != nil {
|
||||||
|
ops = append(ops, c.Lower)
|
||||||
|
}
|
||||||
|
if c.Upper != nil {
|
||||||
|
ops = append(ops, c.Upper)
|
||||||
|
}
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
func (c *SliceIntersectionConstraint) Operands() []ssa.Value { return []ssa.Value{c.X} }
|
||||||
|
func (c *SliceLengthConstraint) Operands() []ssa.Value { return []ssa.Value{c.X} }
|
||||||
|
func (c *MakeSliceConstraint) Operands() []ssa.Value { return []ssa.Value{c.Size} }
|
||||||
|
func (s *SliceIntervalConstraint) Operands() []ssa.Value { return nil }
|
||||||
|
|
||||||
|
func (c *SliceAppendConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = append(%s, %s)", c.Y().Name(), c.A.Name(), c.B.Name())
|
||||||
|
}
|
||||||
|
func (c *SliceSliceConstraint) String() string {
|
||||||
|
var lname, uname string
|
||||||
|
if c.Lower != nil {
|
||||||
|
lname = c.Lower.Name()
|
||||||
|
}
|
||||||
|
if c.Upper != nil {
|
||||||
|
uname = c.Upper.Name()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s[%s:%s]", c.X.Name(), lname, uname)
|
||||||
|
}
|
||||||
|
func (c *ArraySliceConstraint) String() string {
|
||||||
|
var lname, uname string
|
||||||
|
if c.Lower != nil {
|
||||||
|
lname = c.Lower.Name()
|
||||||
|
}
|
||||||
|
if c.Upper != nil {
|
||||||
|
uname = c.Upper.Name()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s[%s:%s]", c.X.Name(), lname, uname)
|
||||||
|
}
|
||||||
|
func (c *SliceIntersectionConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = %s.%t ⊓ %s", c.Y().Name(), c.X.Name(), c.Y().(*ssa.Sigma).Branch, c.I)
|
||||||
|
}
|
||||||
|
func (c *SliceLengthConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = len(%s)", c.Y().Name(), c.X.Name())
|
||||||
|
}
|
||||||
|
func (c *MakeSliceConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = make(slice, %s)", c.Y().Name(), c.Size.Name())
|
||||||
|
}
|
||||||
|
func (c *SliceIntervalConstraint) String() string { return fmt.Sprintf("%s = %s", c.Y().Name(), c.I) }
|
||||||
|
|
||||||
|
func (c *SliceAppendConstraint) Eval(g *Graph) Range {
|
||||||
|
l1 := g.Range(c.A).(SliceInterval).Length
|
||||||
|
var l2 IntInterval
|
||||||
|
switch r := g.Range(c.B).(type) {
|
||||||
|
case SliceInterval:
|
||||||
|
l2 = r.Length
|
||||||
|
case StringInterval:
|
||||||
|
l2 = r.Length
|
||||||
|
default:
|
||||||
|
return SliceInterval{}
|
||||||
|
}
|
||||||
|
if !l1.IsKnown() || !l2.IsKnown() {
|
||||||
|
return SliceInterval{}
|
||||||
|
}
|
||||||
|
return SliceInterval{
|
||||||
|
Length: l1.Add(l2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *SliceSliceConstraint) Eval(g *Graph) Range {
|
||||||
|
lr := NewIntInterval(NewZ(0), NewZ(0))
|
||||||
|
if c.Lower != nil {
|
||||||
|
lr = g.Range(c.Lower).(IntInterval)
|
||||||
|
}
|
||||||
|
ur := g.Range(c.X).(SliceInterval).Length
|
||||||
|
if c.Upper != nil {
|
||||||
|
ur = g.Range(c.Upper).(IntInterval)
|
||||||
|
}
|
||||||
|
if !lr.IsKnown() || !ur.IsKnown() {
|
||||||
|
return SliceInterval{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := []Z{
|
||||||
|
ur.Lower.Sub(lr.Lower),
|
||||||
|
ur.Upper.Sub(lr.Lower),
|
||||||
|
ur.Lower.Sub(lr.Upper),
|
||||||
|
ur.Upper.Sub(lr.Upper),
|
||||||
|
}
|
||||||
|
// TODO(dh): if we don't truncate lengths to 0 we might be able to
|
||||||
|
// easily detect slices with high < low. we'd need to treat -∞
|
||||||
|
// specially, though.
|
||||||
|
for i, l := range ls {
|
||||||
|
if l.Sign() == -1 {
|
||||||
|
ls[i] = NewZ(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliceInterval{
|
||||||
|
Length: NewIntInterval(MinZ(ls...), MaxZ(ls...)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *ArraySliceConstraint) Eval(g *Graph) Range {
|
||||||
|
lr := NewIntInterval(NewZ(0), NewZ(0))
|
||||||
|
if c.Lower != nil {
|
||||||
|
lr = g.Range(c.Lower).(IntInterval)
|
||||||
|
}
|
||||||
|
var l int64
|
||||||
|
switch typ := c.X.Type().(type) {
|
||||||
|
case *types.Array:
|
||||||
|
l = typ.Len()
|
||||||
|
case *types.Pointer:
|
||||||
|
l = typ.Elem().(*types.Array).Len()
|
||||||
|
}
|
||||||
|
ur := NewIntInterval(NewZ(l), NewZ(l))
|
||||||
|
if c.Upper != nil {
|
||||||
|
ur = g.Range(c.Upper).(IntInterval)
|
||||||
|
}
|
||||||
|
if !lr.IsKnown() || !ur.IsKnown() {
|
||||||
|
return SliceInterval{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := []Z{
|
||||||
|
ur.Lower.Sub(lr.Lower),
|
||||||
|
ur.Upper.Sub(lr.Lower),
|
||||||
|
ur.Lower.Sub(lr.Upper),
|
||||||
|
ur.Upper.Sub(lr.Upper),
|
||||||
|
}
|
||||||
|
// TODO(dh): if we don't truncate lengths to 0 we might be able to
|
||||||
|
// easily detect slices with high < low. we'd need to treat -∞
|
||||||
|
// specially, though.
|
||||||
|
for i, l := range ls {
|
||||||
|
if l.Sign() == -1 {
|
||||||
|
ls[i] = NewZ(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliceInterval{
|
||||||
|
Length: NewIntInterval(MinZ(ls...), MaxZ(ls...)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *SliceIntersectionConstraint) Eval(g *Graph) Range {
|
||||||
|
xi := g.Range(c.X).(SliceInterval)
|
||||||
|
if !xi.IsKnown() {
|
||||||
|
return c.I
|
||||||
|
}
|
||||||
|
return SliceInterval{
|
||||||
|
Length: xi.Length.Intersection(c.I),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *SliceLengthConstraint) Eval(g *Graph) Range {
|
||||||
|
i := g.Range(c.X).(SliceInterval).Length
|
||||||
|
if !i.IsKnown() {
|
||||||
|
return NewIntInterval(NewZ(0), PInfinity)
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
func (c *MakeSliceConstraint) Eval(g *Graph) Range {
|
||||||
|
i, ok := g.Range(c.Size).(IntInterval)
|
||||||
|
if !ok {
|
||||||
|
return SliceInterval{NewIntInterval(NewZ(0), PInfinity)}
|
||||||
|
}
|
||||||
|
if i.Lower.Sign() == -1 {
|
||||||
|
i.Lower = NewZ(0)
|
||||||
|
}
|
||||||
|
return SliceInterval{i}
|
||||||
|
}
|
||||||
|
func (c *SliceIntervalConstraint) Eval(*Graph) Range { return SliceInterval{c.I} }
|
258
vendor/honnef.co/go/tools/staticcheck/vrp/string.go
vendored
Normal file
258
vendor/honnef.co/go/tools/staticcheck/vrp/string.go
vendored
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
package vrp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
|
||||||
|
"honnef.co/go/tools/ssa"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StringInterval struct {
|
||||||
|
Length IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StringInterval) Union(other Range) Range {
|
||||||
|
i, ok := other.(StringInterval)
|
||||||
|
if !ok {
|
||||||
|
i = StringInterval{EmptyIntInterval}
|
||||||
|
}
|
||||||
|
if s.Length.Empty() || !s.Length.IsKnown() {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
if i.Length.Empty() || !i.Length.IsKnown() {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return StringInterval{
|
||||||
|
Length: s.Length.Union(i.Length).(IntInterval),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StringInterval) String() string {
|
||||||
|
return s.Length.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StringInterval) IsKnown() bool {
|
||||||
|
return s.Length.IsKnown()
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringSliceConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
Lower ssa.Value
|
||||||
|
Upper ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringIntersectionConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
ranges Ranges
|
||||||
|
A ssa.Value
|
||||||
|
B ssa.Value
|
||||||
|
Op token.Token
|
||||||
|
I IntInterval
|
||||||
|
resolved bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringConcatConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
A ssa.Value
|
||||||
|
B ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringLengthConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
X ssa.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringIntervalConstraint struct {
|
||||||
|
aConstraint
|
||||||
|
I IntInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStringSliceConstraint(x, lower, upper, y ssa.Value) Constraint {
|
||||||
|
return &StringSliceConstraint{NewConstraint(y), x, lower, upper}
|
||||||
|
}
|
||||||
|
func NewStringIntersectionConstraint(a, b ssa.Value, op token.Token, ranges Ranges, y ssa.Value) Constraint {
|
||||||
|
return &StringIntersectionConstraint{
|
||||||
|
aConstraint: NewConstraint(y),
|
||||||
|
ranges: ranges,
|
||||||
|
A: a,
|
||||||
|
B: b,
|
||||||
|
Op: op,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func NewStringConcatConstraint(a, b, y ssa.Value) Constraint {
|
||||||
|
return &StringConcatConstraint{NewConstraint(y), a, b}
|
||||||
|
}
|
||||||
|
func NewStringLengthConstraint(x ssa.Value, y ssa.Value) Constraint {
|
||||||
|
return &StringLengthConstraint{NewConstraint(y), x}
|
||||||
|
}
|
||||||
|
func NewStringIntervalConstraint(i IntInterval, y ssa.Value) Constraint {
|
||||||
|
return &StringIntervalConstraint{NewConstraint(y), i}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StringSliceConstraint) Operands() []ssa.Value {
|
||||||
|
vs := []ssa.Value{c.X}
|
||||||
|
if c.Lower != nil {
|
||||||
|
vs = append(vs, c.Lower)
|
||||||
|
}
|
||||||
|
if c.Upper != nil {
|
||||||
|
vs = append(vs, c.Upper)
|
||||||
|
}
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
func (c *StringIntersectionConstraint) Operands() []ssa.Value { return []ssa.Value{c.A} }
|
||||||
|
func (c StringConcatConstraint) Operands() []ssa.Value { return []ssa.Value{c.A, c.B} }
|
||||||
|
func (c *StringLengthConstraint) Operands() []ssa.Value { return []ssa.Value{c.X} }
|
||||||
|
func (s *StringIntervalConstraint) Operands() []ssa.Value { return nil }
|
||||||
|
|
||||||
|
func (c *StringSliceConstraint) String() string {
|
||||||
|
var lname, uname string
|
||||||
|
if c.Lower != nil {
|
||||||
|
lname = c.Lower.Name()
|
||||||
|
}
|
||||||
|
if c.Upper != nil {
|
||||||
|
uname = c.Upper.Name()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s[%s:%s]", c.X.Name(), lname, uname)
|
||||||
|
}
|
||||||
|
func (c *StringIntersectionConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = %s %s %s (%t branch)", c.Y().Name(), c.A.Name(), c.Op, c.B.Name(), c.Y().(*ssa.Sigma).Branch)
|
||||||
|
}
|
||||||
|
func (c StringConcatConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = %s + %s", c.Y().Name(), c.A.Name(), c.B.Name())
|
||||||
|
}
|
||||||
|
func (c *StringLengthConstraint) String() string {
|
||||||
|
return fmt.Sprintf("%s = len(%s)", c.Y().Name(), c.X.Name())
|
||||||
|
}
|
||||||
|
func (c *StringIntervalConstraint) String() string { return fmt.Sprintf("%s = %s", c.Y().Name(), c.I) }
|
||||||
|
|
||||||
|
func (c *StringSliceConstraint) Eval(g *Graph) Range {
|
||||||
|
lr := NewIntInterval(NewZ(0), NewZ(0))
|
||||||
|
if c.Lower != nil {
|
||||||
|
lr = g.Range(c.Lower).(IntInterval)
|
||||||
|
}
|
||||||
|
ur := g.Range(c.X).(StringInterval).Length
|
||||||
|
if c.Upper != nil {
|
||||||
|
ur = g.Range(c.Upper).(IntInterval)
|
||||||
|
}
|
||||||
|
if !lr.IsKnown() || !ur.IsKnown() {
|
||||||
|
return StringInterval{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := []Z{
|
||||||
|
ur.Lower.Sub(lr.Lower),
|
||||||
|
ur.Upper.Sub(lr.Lower),
|
||||||
|
ur.Lower.Sub(lr.Upper),
|
||||||
|
ur.Upper.Sub(lr.Upper),
|
||||||
|
}
|
||||||
|
// TODO(dh): if we don't truncate lengths to 0 we might be able to
|
||||||
|
// easily detect slices with high < low. we'd need to treat -∞
|
||||||
|
// specially, though.
|
||||||
|
for i, l := range ls {
|
||||||
|
if l.Sign() == -1 {
|
||||||
|
ls[i] = NewZ(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringInterval{
|
||||||
|
Length: NewIntInterval(MinZ(ls...), MaxZ(ls...)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *StringIntersectionConstraint) Eval(g *Graph) Range {
|
||||||
|
var l IntInterval
|
||||||
|
switch r := g.Range(c.A).(type) {
|
||||||
|
case StringInterval:
|
||||||
|
l = r.Length
|
||||||
|
case IntInterval:
|
||||||
|
l = r
|
||||||
|
}
|
||||||
|
|
||||||
|
if !l.IsKnown() {
|
||||||
|
return StringInterval{c.I}
|
||||||
|
}
|
||||||
|
return StringInterval{
|
||||||
|
Length: l.Intersection(c.I),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c StringConcatConstraint) Eval(g *Graph) Range {
|
||||||
|
i1, i2 := g.Range(c.A).(StringInterval), g.Range(c.B).(StringInterval)
|
||||||
|
if !i1.Length.IsKnown() || !i2.Length.IsKnown() {
|
||||||
|
return StringInterval{}
|
||||||
|
}
|
||||||
|
return StringInterval{
|
||||||
|
Length: i1.Length.Add(i2.Length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *StringLengthConstraint) Eval(g *Graph) Range {
|
||||||
|
i := g.Range(c.X).(StringInterval).Length
|
||||||
|
if !i.IsKnown() {
|
||||||
|
return NewIntInterval(NewZ(0), PInfinity)
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
func (c *StringIntervalConstraint) Eval(*Graph) Range { return StringInterval{c.I} }
|
||||||
|
|
||||||
|
func (c *StringIntersectionConstraint) Futures() []ssa.Value {
|
||||||
|
return []ssa.Value{c.B}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StringIntersectionConstraint) Resolve() {
|
||||||
|
if (c.A.Type().Underlying().(*types.Basic).Info() & types.IsString) != 0 {
|
||||||
|
// comparing two strings
|
||||||
|
r, ok := c.ranges[c.B].(StringInterval)
|
||||||
|
if !ok {
|
||||||
|
c.I = NewIntInterval(NewZ(0), PInfinity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch c.Op {
|
||||||
|
case token.EQL:
|
||||||
|
c.I = r.Length
|
||||||
|
case token.GTR, token.GEQ:
|
||||||
|
c.I = NewIntInterval(r.Length.Lower, PInfinity)
|
||||||
|
case token.LSS, token.LEQ:
|
||||||
|
c.I = NewIntInterval(NewZ(0), r.Length.Upper)
|
||||||
|
case token.NEQ:
|
||||||
|
default:
|
||||||
|
panic("unsupported op " + c.Op.String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r, ok := c.ranges[c.B].(IntInterval)
|
||||||
|
if !ok {
|
||||||
|
c.I = NewIntInterval(NewZ(0), PInfinity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// comparing two lengths
|
||||||
|
switch c.Op {
|
||||||
|
case token.EQL:
|
||||||
|
c.I = r
|
||||||
|
case token.GTR:
|
||||||
|
c.I = NewIntInterval(r.Lower.Add(NewZ(1)), PInfinity)
|
||||||
|
case token.GEQ:
|
||||||
|
c.I = NewIntInterval(r.Lower, PInfinity)
|
||||||
|
case token.LSS:
|
||||||
|
c.I = NewIntInterval(NInfinity, r.Upper.Sub(NewZ(1)))
|
||||||
|
case token.LEQ:
|
||||||
|
c.I = NewIntInterval(NInfinity, r.Upper)
|
||||||
|
case token.NEQ:
|
||||||
|
default:
|
||||||
|
panic("unsupported op " + c.Op.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StringIntersectionConstraint) IsKnown() bool {
|
||||||
|
return c.I.IsKnown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StringIntersectionConstraint) MarkUnresolved() {
|
||||||
|
c.resolved = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StringIntersectionConstraint) MarkResolved() {
|
||||||
|
c.resolved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StringIntersectionConstraint) IsResolved() bool {
|
||||||
|
return c.resolved
|
||||||
|
}
|
1049
vendor/honnef.co/go/tools/staticcheck/vrp/vrp.go
vendored
Normal file
1049
vendor/honnef.co/go/tools/staticcheck/vrp/vrp.go
vendored
Normal file
File diff suppressed because it is too large
Load diff
7
vendor/modules.txt
vendored
7
vendor/modules.txt
vendored
|
@ -196,13 +196,20 @@ gopkg.in/testfixtures.v2
|
||||||
gopkg.in/yaml.v2
|
gopkg.in/yaml.v2
|
||||||
# honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3
|
# honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3
|
||||||
honnef.co/go/tools/cmd/gosimple
|
honnef.co/go/tools/cmd/gosimple
|
||||||
|
honnef.co/go/tools/cmd/staticcheck
|
||||||
honnef.co/go/tools/cmd/unused
|
honnef.co/go/tools/cmd/unused
|
||||||
honnef.co/go/tools/lint/lintutil
|
honnef.co/go/tools/lint/lintutil
|
||||||
honnef.co/go/tools/simple
|
honnef.co/go/tools/simple
|
||||||
|
honnef.co/go/tools/staticcheck
|
||||||
honnef.co/go/tools/unused
|
honnef.co/go/tools/unused
|
||||||
honnef.co/go/tools/lint
|
honnef.co/go/tools/lint
|
||||||
honnef.co/go/tools/version
|
honnef.co/go/tools/version
|
||||||
honnef.co/go/tools/internal/sharedcheck
|
honnef.co/go/tools/internal/sharedcheck
|
||||||
honnef.co/go/tools/lint/lintdsl
|
honnef.co/go/tools/lint/lintdsl
|
||||||
|
honnef.co/go/tools/deprecated
|
||||||
|
honnef.co/go/tools/functions
|
||||||
honnef.co/go/tools/ssa
|
honnef.co/go/tools/ssa
|
||||||
|
honnef.co/go/tools/staticcheck/vrp
|
||||||
honnef.co/go/tools/ssa/ssautil
|
honnef.co/go/tools/ssa/ssautil
|
||||||
|
honnef.co/go/tools/callgraph
|
||||||
|
honnef.co/go/tools/callgraph/static
|
||||||
|
|
Loading…
Reference in a new issue