Page MenuHomeSealhub

No OneTemporary

diff --git a/README.md b/README.md
index 9191446..0eecf3b 100644
--- a/README.md
+++ b/README.md
@@ -1,128 +1,128 @@
# Arcanist Linters
[![Build Status](https://travis-ci.org/pinterest/arcanist-linters.svg)](https://travis-ci.org/pinterest/arcanist-linters)
This is a collection of custom [Arcanist][] linters that we've written at
Pinterest.
## Linters
### Apache Thrift Linter
Lints for errors in [Apache Thrift](http://thrift.apache.org/) IDL (schema)
files using the `thrift` compiler.
```json
{
"type": "thrift",
"include": "(\\.thrift$)",
"flags": [
"--allow-64bit-consts"
],
"version": ">= 0.9.0",
"thrift.generators": [
"py:dynamic,utf8strings,new_style,slots",
"java",
"go",
"erl"
],
"thrift.includes": [
".",
"common"
]
}
```
### Go Vet Linter
Uses the [Go vet command](https://golang.org/cmd/vet/) to lint for suspicious
code constructs.
```json
{
"type": "govet",
"include": "(^src/example.com/.*\\.go$)"
}
```
### Python Imports Linter
Lints for illegal Python module imports.
```json
{
"type": "python-imports",
"python-imports.pattern": "(mock)",
"include": "(\\.py$)",
"exclude": "(^tests/)"
}
```
### Python Requirements Linter
Ensures Python package requirements in [requirements.txt files][req-txt] are
-sorted and unique.
+sorted, unique, and pinned to exact versions.
```json
{
"type": "requirements-txt",
"include": "(requirements.txt$)"
}
```
[req-txt]: https://pip.readthedocs.org/en/latest/user_guide/#requirements-files
## Installation
In short, you'll need to add this repository to your local machine and tell
Arcanist to load the extension. You either can do this globally or on a
per-project basis.
Once installed, the individual linters can be enabled and configured via the
project's `.arclint` file. See the [Arcanist Lint User Guide][lint-guide] for
details.
## Global Installation
Arcanist can load modules from an absolute path, but because it also searches
for modules one level up from itself on the filesystem, it's convenient to
clone this repository at the same level as `arcanist` and `libphutil`.
```
$ git clone https://github.com/pinterest/arcanist-linters.git pinterest-linters
$ ls
arcanist
pinterest-linters
libphutil
```
Then, tell Arcanist to load the module by editing `~/.arcconfig` (or
`/etc/arcconfig`):
```json
{
"load": ["pinterest-linters"]
}
```
## Project Installation
You can also load `arcanist-linters` on a per-project basis. In that case,
using a [git submodule](https://git-scm.com/docs/git-submodule) is probably
the most convenient approach.
```
$ git submodule add https://github.com/pinterest/arcanist-linters.git .pinterest-linters
$ git submodule update --init
```
Then, enable the module in your project-level `.arcconfig` file:
```json
{
"load": [".pinterest-linters"]
}
```
[Arcanist]: https://secure.phabricator.com/book/phabricator/article/arcanist/
[lint-guide]: https://secure.phabricator.com/book/phabricator/article/arcanist_lint/
diff --git a/src/PythonRequirementsLinter.php b/src/PythonRequirementsLinter.php
index 8c751dd..442d8d0 100644
--- a/src/PythonRequirementsLinter.php
+++ b/src/PythonRequirementsLinter.php
@@ -1,148 +1,168 @@
<?php
/**
* Copyright 2016 Pinterest, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Ensures Python package requirements are sorted and unique.
*/
final class PythonRequirementsLinter extends ArcanistLinter {
const LINT_DUPLICATES = 1;
const LINT_UNSORTED = 2;
+ const LINT_UNPINNED = 3;
public function getInfoName() {
return 'Python requirements.txt Linter';
}
public function getInfoDescription() {
return pht('Ensures package requirements are sorted and unique.');
}
public function getInfoURI() {
return 'https://pip.readthedocs.org/en/latest/user_guide/#requirements-files';
}
public function getLinterName() {
return 'REQUIREMENTS-TXT';
}
public function getLinterConfigurationName() {
return 'requirements-txt';
}
public function getLintSeverityMap() {
return array(
self::LINT_DUPLICATES => ArcanistLintSeverity::SEVERITY_ERROR,
self::LINT_UNSORTED => ArcanistLintSeverity::SEVERITY_WARNING,
+ self::LINT_UNPINNED => ArcanistLintSeverity::SEVERITY_WARNING,
);
}
public function getLintNameMap() {
return array(
self::LINT_DUPLICATES => pht('Duplicate package requirement'),
self::LINT_UNSORTED => pht('Unsorted package requirement'),
+ self::LINT_UNPINNED => pht('Unpinned package version'),
);
}
private function parseRequirement($line) {
# PEP 508 (https://www.python.org/dev/peps/pep-0508/)
$regex = "/^(?P<name>[[:alnum:]][[:alnum:]-_.]*)".
"(?:\s*(?P<cmp>(~=|==|!=|<=|>=|<|>|===))\s*".
"(?P<version>[[:alnum:]-_.*+!]+))?/";
$matches = array();
if (preg_match($regex, $line, $matches)) {
return $matches;
}
return null;
}
private function formatRequirement($req) {
return sprintf("%s%s%s", $req['name'], $req['cmp'], $req['version']);
}
private function lintDuplicates(array $reqs) {
$packages = array();
foreach ($reqs as $lineno => $req) {
$package = strtolower($req['name']);
if (array_key_exists($package, $packages)) {
$first = $packages[$package];
$this->raiseLintAtLine(
$lineno,
1,
self::LINT_DUPLICATES,
pht(
'This line contains a duplicate package requirement for "%s". '.
'The first reference appears on line %d ("%s")',
$package, $first[0], $this->formatRequirement($first[1])),
$package);
} else {
$packages[$package] = array($lineno, $req);
}
}
}
private function lintUnsorted(array $reqs) {
$last_lineno = 0;
$last_package = null;
foreach ($reqs as $lineno => $req) {
// Only require consecutive requirement lines to be ordered. If we're
// skipping over some other lines, clear $last_package to start a new
// ordered "section".
if ($lineno > $last_lineno + 1) {
$last_package = null;
}
$package = $req['name'];
if (strnatcasecmp($package, $last_package) <= 0) {
$this->raiseLintAtLine(
$lineno,
1,
self::LINT_UNSORTED,
pht(
"This line doesn't appear in sorted order. Please keep ".
"package requirements ordered alphabetically."));
}
$last_lineno = $lineno;
$last_package = $package;
}
}
+ private function lintUnpinned(array $reqs) {
+ foreach ($reqs as $lineno => $req) {
+ if ($req['cmp'] != '==') {
+ $this->raiseLintAtLine(
+ $lineno,
+ 1,
+ self::LINT_UNPINNED,
+ pht(
+ "This package requirement isn't pinned to an exact version. ".
+ "Use the `==` operator to specify a version."));
+ }
+ }
+ }
+
public function lintPath($path) {
$lines = phutil_split_lines($this->getData($path), false);
$lines = array_map('trim', $lines);
// Build a sparse array mapping line numbers to parsed requirements.
$reqs = array();
foreach ($lines as $lineno => $line) {
$req = $this->parseRequirement($line);
if (!empty($req)) {
$reqs[$lineno + 1] = $req;
}
}
if ($this->isMessageEnabled(self::LINT_DUPLICATES)) {
$this->lintDuplicates($reqs);
}
if ($this->isMessageEnabled(self::LINT_UNSORTED)) {
$this->lintUnsorted($reqs);
}
+ if ($this->isMessageEnabled(self::LINT_UNPINNED)) {
+ $this->lintUnpinned($reqs);
+ }
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 8, 06:23 (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034091
Default Alt Text
(8 KB)

Event Timeline